diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e27991ebf8822ddb357c4dbc44c51e54e237a3d..4467ece33e62e9f29dac413c1828847ed9b766c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,17 @@ +## new in jupyter-openbis-extension 0.4.0 + +- made it compatible to pybis-1.9.x +- minor bugfixes and improvements + +## new in jupyter-openbis-extension 0.3.0 + +- removed search-as-you-type feature, as it is not mature yet +- fixed notebook path problem in special environments +- improved download path +- connection dialog: show that connection is being established +- upload dialog: improved file chooser +- upload dialog: save notebook automatically before upload + ## new in jupyter-openbis-extension 0.2.4 - improved entity selection diff --git a/jupyter-openbis-extension/__init__.py b/jupyter-openbis-extension/__init__.py index d3d298723cee99d050e7464c5bac6a4660c7483d..c4c2f3ea1bd3b4b9375e676f33f3c6fd73ebcab2 100644 --- a/jupyter-openbis-extension/__init__.py +++ b/jupyter-openbis-extension/__init__.py @@ -1,7 +1,7 @@ name = 'jupyter-openbis-extension.server' __author__ = 'Swen Vermeul' __email__ = 'swen@ethz.ch' -__version__ = '0.2.4' +__version__ = '0.4.0' def _jupyter_server_extension_paths(): return [{ diff --git a/jupyter-openbis-extension/connection.py b/jupyter-openbis-extension/connection.py index be36cbb321ce71e2b164d9d06762937d18912706..557bc4966487975ee684799b4d2e2473fe4264ac 100644 --- a/jupyter-openbis-extension/connection.py +++ b/jupyter-openbis-extension/connection.py @@ -12,6 +12,7 @@ def register_connection(connection_info): verify_certificates = connection_info.get('verify_certificates', False), username = connection_info.get('username'), password = connection_info.get('password'), + http_only = connection_info.get('http_only', False), status = 'not connected', ) openbis_connections[conn.name] = conn @@ -32,7 +33,8 @@ class OpenBISConnection: openbis = Openbis( url = self.url, - verify_certificates = self.verify_certificates + verify_certificates = self.verify_certificates, + allow_http_but_do_not_use_this_in_production_and_only_within_safe_networks = self.http_only ) self.openbis = openbis self.status = "not connected" @@ -71,6 +73,14 @@ class OpenBISConnection: class OpenBISConnections(IPythonHandler): + def _notebook_dir(self): + notebook_dir = os.getcwd() + if 'SingleUserNotebookApp' in self.config and 'notebook_dir' in self.config.SingleUserNotebookApp: + notebook_dir = self.config.SingleUserNotebookApp.notebook_dir + elif 'notebook_dir' in self.config.NotebookApp: + notebook_dir = self.config.NotebookApp.notebook_dir + return notebook_dir + def post(self): """create a new connection @@ -96,9 +106,9 @@ class OpenBISConnections(IPythonHandler): connections.append(conn.get_info()) self.write({ - 'status' : 200, - 'connections': connections, - 'cwd' : os.getcwd() + 'status' : 200, + 'connections' : connections, + 'notebook_dir' : self._notebook_dir() }) return @@ -107,6 +117,14 @@ class OpenBISConnectionHandler(IPythonHandler): """Handle the requests to /openbis/conn """ + def _notebook_dir(self): + notebook_dir = os.getcwd() + if 'SingleUserNotebookApp' in self.config and 'notebook_dir' in self.config.SingleUserNotebookApp: + notebook_dir = self.config.SingleUserNotebookApp.notebook_dir + elif 'notebook_dir' in self.config.NotebookApp: + notebook_dir = self.config.NotebookApp.notebook_dir + return notebook_dir + def put(self, connection_name): """reconnect to a current connection :return: an updated connection object @@ -136,11 +154,16 @@ class OpenBISConnectionHandler(IPythonHandler): "reason": "Incorrect username or password for {}".format(connection_name) }) return + except Exception: + self.set_status(500) + self.write({ + "reason": "General Network Error" + }) self.write({ 'status' : 200, - 'connection': conn.get_info(), - 'cwd' : os.getcwd() + 'connection' : conn.get_info(), + '' : self._notebook_dir() }) def get(self, connection_name): @@ -159,9 +182,10 @@ class OpenBISConnectionHandler(IPythonHandler): conn.check_status() self.write({ - 'status' : 200, - 'connection': conn.get_info(), - 'cwd' : os.getcwd() + 'status' : 200, + 'connection' : conn.get_info(), + 'noteboook_dir' : self._notebook_dir() }) return + diff --git a/jupyter-openbis-extension/dataset.py b/jupyter-openbis-extension/dataset.py index 91c6a89488dead78c42bef4fba4292b6122bfdf3..884c519c1a5159ab260f73208b55dae2dc3e0069 100644 --- a/jupyter-openbis-extension/dataset.py +++ b/jupyter-openbis-extension/dataset.py @@ -6,7 +6,6 @@ from .connection import openbis_connections class DataSetDownloadHandler(IPythonHandler): """Handle the requests for /openbis/dataset/connection/permId""" - def download_data(self, conn, permId, downloadPath=None): if not conn.is_session_active(): try: @@ -46,6 +45,7 @@ class DataSetDownloadHandler(IPythonHandler): 'dataStore' : dataset.dataStore, 'location' : dataset.physicalData.location, 'size' : dataset.physicalData.size, + 'files' : dataset.file_list, 'statusText': 'Data for DataSet {} was successfully downloaded to: {}.'.format(dataset.permId, path) }) @@ -69,6 +69,15 @@ class DataSetDownloadHandler(IPythonHandler): class DataSetTypesHandler(IPythonHandler): def get(self, **params): """Handle a request to /openbis/datasetTypes/connection_name + This meta-metadata is used in the dataset upload dialog (uploadDialog.js) + to check data directly in the UI + + Returns all datasetTypes of a given connection + - with all assigned properties + - with some details about the property types + - with the vocabulary, if exists + + The result will be cached, as it is a costly operation with many fetches """ try: @@ -80,26 +89,44 @@ class DataSetTypesHandler(IPythonHandler): }) return + if getattr(conn, 'dataset_types', False): + self.write({ + "dataSetTypes": conn.dataset_types + }) + return + try: dataset_types = conn.openbis.get_dataset_types() - dts = dataset_types.df.to_dict(orient='records') - - # get property assignments for every dataset-type - # and add it to the dataset collection - for dt in dts: - dataset_type = conn.openbis.get_dataset_type(dt['code']) - pa = dataset_type.get_propertyAssignments(including_vocabulary=True) - pa_dicts = pa.to_dict(orient='records') - for pa_dict in pa_dicts: - if pa_dict['dataType'] == 'CONTROLLEDVOCABULARY': - terms = conn.openbis.get_terms(pa_dict['vocabulary']['code']) - pa_dict['terms'] = terms.df[['code','label','description','official','ordinal']].to_dict(orient='records') - dt['propertyAssignments'] = pa_dicts + # get all dataset types + ds_type_dicts = [] + for dt in conn.openbis.get_dataset_types(): + dt_dict = dt.attrs.all() + # get property assignments for every dataset-type + # and add them in the key «propertyAssignments» + pas = dt.get_property_assignments() + pa_dicts = pas.df[['propertyType','mandatory','ordinal','section']].to_dict(orient='records') + dt_dict['propertyAssignments'] = pa_dicts + + for pa_dict in pa_dicts: + # add a few more attributes to the property assignments + pt = conn.openbis.get_property_type(pa_dict['propertyType']) + pa_dict['code'] = pt.code + pa_dict['label'] = pt.label + pa_dict['description'] = pt.description + pa_dict['dataType'] = pt.dataType + # add vocabulary, if exists, as key «terms» + if pt.dataType == 'CONTROLLEDVOCABULARY': + terms = conn.openbis.get_terms(pt.vocabulary) + terms_dict = terms.df[['code','label','description','official','ordinal']].to_dict(orient='records') + pa_dict['terms'] = terms_dict + + ds_type_dicts.append(dt_dict) self.write({ - "dataSetTypes": dts + "dataSetTypes": ds_type_dicts }) + conn.dataset_types = ds_type_dicts return except Exception as e: @@ -114,6 +141,14 @@ class DataSetTypesHandler(IPythonHandler): class DataSetUploadHandler(IPythonHandler): """Handle the POST requests for /openbis/dataset/connection_name""" + def _notebook_dir(self): + notebook_dir = os.getcwd() + if 'SingleUserNotebookApp' in self.config and 'notebook_dir' in self.config.SingleUserNotebookApp: + notebook_dir = self.config.SingleUserNotebookApp.notebook_dir + elif 'notebook_dir' in self.config.NotebookApp: + notebook_dir = self.config.NotebookApp.notebook_dir + return notebook_dir + def upload_data(self, conn, data): if not conn.is_session_active(): try: @@ -165,13 +200,15 @@ class DataSetUploadHandler(IPythonHandler): }) filenames = [] + notebook_dir = self._notebook_dir() for filename in data.get('files'): filename = unquote(filename) - if os.path.isfile(filename): - filenames.append(filename) + full_filename_path = os.path.join(notebook_dir, filename) + if os.path.isfile(full_filename_path): + filenames.append(full_filename_path) else: errors.append({ - "file": "File not found: {}".format(filename) + "file": "File not found: {}".format(full_filename_path) }) try: @@ -183,9 +220,9 @@ class DataSetUploadHandler(IPythonHandler): files = filenames, ) except Exception as e: - print(e) - errors.append({ - "create": 'Error while creating the dataset: {}'.format(e) + self.set_status(500) + self.write({ + "reason": 'Error while creating the dataset: {}'.format(e) }) # try to set the properties @@ -249,34 +286,3 @@ class DataSetUploadHandler(IPythonHandler): self.upload_data(conn=conn,data=data) -class FileListHandler(IPythonHandler): - - def get(self, **params): - """ - Returns the file list of the current working directory - - :param params: - :return: dictionary of files, key is the fully qualified name, - value is the relative name (for display) - """ - - cwd = os.getcwd() - files = {} - for (dirpath, dirnames, filenames) in os.walk(cwd): - if filenames: - for filename in filenames: - # ignore hidden files - if filename.startswith('.'): - continue - # ignore hidden folders - if os.path.relpath(dirpath) != '.' \ - and os.path.relpath(dirpath).startswith('.'): - continue - fqn = os.path.join(dirpath, filename) - files[fqn] = os.path.relpath(fqn, cwd) - - self.set_status(200) - self.write({ - "files": files - }) - diff --git a/jupyter-openbis-extension/server.py b/jupyter-openbis-extension/server.py index 97d4909c5b85e9b1b1b5358257a6e933b0599a0b..6d95bd59f0cf163c1bc92f74ede90df13e633634 100644 --- a/jupyter-openbis-extension/server.py +++ b/jupyter-openbis-extension/server.py @@ -3,7 +3,7 @@ import os import yaml from .connection import OpenBISConnections, OpenBISConnectionHandler, register_connection -from .dataset import DataSetTypesHandler, DataSetDownloadHandler, DataSetUploadHandler, FileListHandler +from .dataset import DataSetTypesHandler, DataSetDownloadHandler, DataSetUploadHandler from .sample import SampleHandler @@ -48,6 +48,7 @@ def load_jupyter_server_extension(nb_server_app): filename = 'openbis-connections.yaml' ) + for connection_info in connections: conn = register_connection(connection_info) print("Registered: {}".format(conn.url)) @@ -66,14 +67,6 @@ def load_jupyter_server_extension(nb_server_app): host_pattern = '.*$' base_url = web_app.settings['base_url'] - # get the file list - web_app.add_handlers( - host_pattern, - [(url_path_join( base_url, '/general/filelist'), - FileListHandler - )] - ) - # DataSet download web_app.add_handlers( @@ -139,3 +132,4 @@ def load_jupyter_server_extension(nb_server_app): ) + diff --git a/jupyter-openbis-extension/static/common.js b/jupyter-openbis-extension/static/common.js index 15280a1e178c749e5e17292270e226bd061452ca..edcb4af10533d72158d37de5d428a2d27025c3a5 100644 --- a/jupyter-openbis-extension/static/common.js +++ b/jupyter-openbis-extension/static/common.js @@ -39,9 +39,18 @@ define([ return ""; } + function createErrorElement() { + var element = document.createElement("STRONG") + element.textContent = "" + element.style.marginLeft = "8px" + element.style.color = "red" + return element + } + return { createFeedback: createFeedback, - getCookie: getCookie + getCookie: getCookie, + createErrorElement: createErrorElement } } ) \ No newline at end of file diff --git a/jupyter-openbis-extension/static/connectionDialog.js b/jupyter-openbis-extension/static/connectionDialog.js index 40c88225a504b0971efb6bb05afcbed4a8c652da..c8d01ab61e699f759c8569df0dea540df3242bf2 100644 --- a/jupyter-openbis-extension/static/connectionDialog.js +++ b/jupyter-openbis-extension/static/connectionDialog.js @@ -7,34 +7,35 @@ define( ], function (dialog, $, state, connections) { - var currentDownloadPath = null + let currentDownloadPath = null; function show_available_connections(env, data, conn_table) { + if (!currentDownloadPath) { currentDownloadPath = data.cwd } - var table = document.createElement("TABLE") + let table = document.createElement("TABLE"); table.className = 'table-bordered table-striped table-condensed' - var thead = table.createTHead() - var thead_row = thead.insertRow(0) - var titles = ['', 'Name', 'URL', 'Status', 'Username / Password'] + let thead = table.createTHead(); + let thead_row = thead.insertRow(0) + let titles = ['', 'Name', 'URL', 'Status', 'Username / Password'] for (title of titles) { thead_row.insertCell().textContent = title } tbody = table.createTBody() - var getConnectionByName = function(name) { - for (connection of data.connections) { - if(connection.name === name) { - return connection; - } - } - } - + let getConnectionByName = function (name) { + for (connection of data.connections) { + if (connection.name === name) { + return connection; + } + } + }; + for (connection of data.connections) { - var conn = document.createElement("INPUT") + let conn = document.createElement("INPUT") conn.type = "radio" conn.name = "connection_name" conn.value = connection.name @@ -46,14 +47,28 @@ define( state.connection.candidateDTO = getConnectionByName(state.connection.candidateName); } - var row = tbody.insertRow() + let row = tbody.insertRow() row.insertCell().appendChild(conn) - row.insertCell().textContent = connection.name - row.insertCell().textContent = connection.url + let nameCell = row.insertCell() + nameCell.textContent = connection.name + nameCell.onclick = function () { + let radio = this.parentElement.firstElementChild.firstElementChild + radio.checked = 1 + state.connection.candidateName = radio.value + state.connection.candidateDTO = getConnectionByName(state.connection.candidateName); + } + let urlCell = row.insertCell() + urlCell.textContent = connection.url + urlCell.onclick = function () { + let radio = this.parentElement.firstElementChild.firstElementChild + radio.checked = 1 + state.connection.candidateName = radio.value + state.connection.candidateDTO = getConnectionByName(state.connection.candidateName); + } - var status_cell = row.insertCell() + let status_cell = row.insertCell() - var status_badge = document.createElement("SPAN") + let status_badge = document.createElement("SPAN") status_badge.id = connection.name + "-badge" status_badge.textContent = connection.status if (connection.status === "connected") { @@ -63,31 +78,33 @@ define( } status_cell.appendChild(status_badge) - var username = document.createElement("INPUT") + let username = document.createElement("INPUT") username.type = "text" username.name = "username" username.autocomplete = "on" username.value = connection.username username.setAttribute("form", connection.name) - var password = document.createElement("INPUT") + let password = document.createElement("INPUT") password.type = "password" password.name = "password" password.autocomplete = "current-password" password.value = connection.password password.setAttribute("form", connection.name) + // Username / Password form - var pwform = document.createElement("FORM") + let pwform = document.createElement("FORM") pwform.id = connection.name pwform.onsubmit = function (event) { - var form_data = new FormData(this) - var status_badge = document.getElementById(this.id + "-badge") + let form_data = new FormData(this) + let status_badge = document.getElementById(this.id + "-badge") + status_badge.textContent = "connecting..." + status_badge.className = "label label-warning" connections.connect(env, this.id, form_data.get("username"), form_data.get("password") ) .then(function (response) { - //console.log(response) if (status_badge.nextElementSibling !== null) { status_badge.parentNode.removeChild(status_badge.nextElementSibling) } @@ -114,7 +131,7 @@ define( } - var connect_button = document.createElement("BUTTON") + let connect_button = document.createElement("BUTTON") connect_button.className = "btn btn-primary btn-xs" connect_button.textContent = "connect" @@ -122,17 +139,24 @@ define( pwform.appendChild(password) pwform.appendChild(connect_button) - var cell = row.insertCell() - cell.appendChild(pwform) + let pwCell = row.insertCell() + pwCell.appendChild(pwform) + + pwCell.onclick = function () { + let radio = this.parentElement.firstElementChild.firstElementChild + radio.checked = 1 + state.connection.candidateName = radio.value + state.connection.candidateDTO = getConnectionByName(state.connection.candidateName); + } } // add row for new connection - var row = tbody.insertRow() + let row = tbody.insertRow() - var conn_form = document.createElement("FORM") + let conn_form = document.createElement("FORM") conn_form.id = "new_connection" conn_form.onsubmit = function (event) { - var inputs = document.querySelectorAll("input[form=new_connection]") + let inputs = document.querySelectorAll("input[form=new_connection]") data = {} for (input of inputs) { @@ -155,7 +179,7 @@ define( }) return false } - var conn_name = document.createElement("INPUT") + let conn_name = document.createElement("INPUT") conn_name.type = "input" conn_name.name = "connection_name" conn_name.setAttribute("form", conn_form.id) @@ -163,7 +187,7 @@ define( row.insertCell().appendChild(conn_form) row.insertCell().appendChild(conn_name) - var conn_url = document.createElement("INPUT") + let conn_url = document.createElement("INPUT") conn_url.type = "input" conn_url.name = "url" conn_url.setAttribute("form", conn_form.id) @@ -171,30 +195,68 @@ define( row.insertCell().appendChild(conn_url) row.insertCell() - var username = document.createElement("INPUT") + let username = document.createElement("INPUT") username.autocomplete = "off" username.type = "text" username.name = "username" username.setAttribute("form", conn_form.id) username.placeholder = "username" - var password = document.createElement("INPUT") + let password = document.createElement("INPUT") password.type = "password" password.name = "password" password.autocomplete = "new-password" password.setAttribute("form", conn_form.id) - var create_btn = document.createElement("BUTTON") + let create_btn = document.createElement("BUTTON") create_btn.setAttribute("form", conn_form.id) create_btn.textContent = "create" - var uname_pw_cell = row.insertCell() + let uname_pw_cell = row.insertCell() uname_pw_cell.appendChild(username) uname_pw_cell.appendChild(password) uname_pw_cell.appendChild(create_btn) conn_table.innerHTML = "" - table_title = document.createElement("STRONG") + let table_title = document.createElement("STRONG") table_title.textContent = "Please choose a connection" + + let working_dir_title = document.createElement("STRONG") + working_dir_title.textContent = "Your working directory " + let working_dir_in = document.createElement("INPUT") + working_dir_in.type = "text" + working_dir_in.name = "working_dir" + working_dir_in.autocomplete = "on" + working_dir_in.style.width = "100%" + + + // calculate the default working directory + // by combining the notebook_dir (from the jupyter configuration) and the relative notebook_path + let re = new RegExp(env.notebook.notebook_name+"$") + rel_path = env.notebook.notebook_path.replace(re, "") + let default_working_dir = "" + if (data.notebook_dir.endsWith('/')) { + default_working_dir = data.notebook_dir + rel_path + } + else { + default_working_dir = data.notebook_dir + "/" + rel_path + } + + working_dir_in.value = state.working_dir ? state.working_dir : default_working_dir + state.working_dir_element = working_dir_in + + let working_dir_reset = document.createElement("A") + working_dir_reset.className = "btn" + working_dir_reset.innerText = "reset to default" + working_dir_reset.onclick = function() { + working_dir_in.value = default_working_dir + } + conn_table.appendChild(table_title) conn_table.appendChild(table) + + conn_table.appendChild(working_dir_title) + conn_table.appendChild(working_dir_reset) + conn_table.appendChild(document.createElement("BR")) + conn_table.append(working_dir_in) + } return { @@ -203,13 +265,12 @@ define( help_index: '', handler: function (env) { conn_table = document.createElement("DIV") - var dst_title = document.createElement("STRONG") + let dst_title = document.createElement("STRONG") dst_title.textContent = "DataSet type" - var dataset_types = document.createElement("SELECT") + let dataset_types = document.createElement("SELECT") dataset_types.id = "dataset_type" dataset_types.className = "form-control select-xs" - var input_fields = document.createElement("DIV") conn_table.id = "openbis_connections" connections.list(env) @@ -220,11 +281,12 @@ define( alert(data.status) }) - var uploadDialogBox = $('<div/>').append(conn_table) + let uploadDialogBox = $('<div/>').append(conn_table) function onOk() { state.connection.name = state.connection.candidateName state.connection.dto = state.connection.candidateDTO + state.working_dir = state.working_dir_element.value } function onCancel() { @@ -250,4 +312,4 @@ define( } } } -) \ No newline at end of file +) diff --git a/jupyter-openbis-extension/static/downloadDialog.js b/jupyter-openbis-extension/static/downloadDialog.js index b948043a7f2c414249b059b0513df21177ad0239..bd251b818e18358f47e3837ded8fe8848461ffe4 100644 --- a/jupyter-openbis-extension/static/downloadDialog.js +++ b/jupyter-openbis-extension/static/downloadDialog.js @@ -3,15 +3,16 @@ define([ "jquery", "./common", "./state", - "./entitySearcher" + //"./entitySearcher" ], - function (dialog, $, common, state, entitySearcher) { + function (dialog, $, common, state) { var spinner = document.createElement("IMG") spinner.className="openbis-feedback" spinner.src="" function showSpinner() { - spinner.src="https://cdnjs.cloudflare.com/ajax/libs/slick-carousel/1.5.8/ajax-loader.gif" + var userName = window.location.pathname.split("/")[2]; + spinner.src = '/user/' + userName+ '/nbextensions/jupyter-openbis-extension/spinner.gif' } function hideSpinner() { spinner.src="" @@ -30,16 +31,13 @@ define([ "dataStore": data.dataStore, "location": data.location, "size": data.size, + "files": data.files, "status": data.statusText } } } function show_datasets_table(env, data, datasets_table, pagingContainer, downloadPath, entityIdentifier) { - if (downloadPath.value === '') { - downloadPath.value = data.cwd - } - var table = document.createElement("TABLE") table.className = "table-bordered table-striped table-condensed text-nowrap" table.style.width = "100%" @@ -87,25 +85,45 @@ define([ const previousCmd = () => getDatasets(env, startWith-5, 5, entityIdentifier, datasets_table, pagingContainer, downloadPath) var previous = document.createElement("A") - var linkText = document.createTextNode("<<< Previous") - previous.appendChild(linkText) - previous.href = "#" - previous.onclick = previousCmd + previous.className = "fas fa fa-caret-left fa-2x" + previous.style.verticalAlign="middle" + if (hasPrevious) { + previous.href = "#" + previous.onclick = previousCmd + } var next = document.createElement("A") - var linkText = document.createTextNode("Next >>>") - next.appendChild(linkText) - next.href = "#" - next.onclick = nextCmd - next.style.float="right" + next.className = "fas fa fa-caret-right fa-2x" + next.style.verticalAlign = "middle" + if (hasNext) { + next.href = "#" + next.onclick = nextCmd + } + + var countStr = document.createElement("STRONG") + var until = startWith + count > totalCount ? totalCount : startWith + count + var startWithStr = startWith + 1 + countStr.innerHTML = startWithStr + "–" + until + "/" + totalCount + countStr.style.paddingLeft = "10px" + countStr.style.paddingRight = "10px" + countStr.style.verticalAlign="middle" var paging = document.createElement("DIV") paging.style.width = "100%" + paging.appendChild(previous) if (hasPrevious) { - paging.appendChild(previous) + previous.style.color = "inherit" + } + else { + previous.style.color="transparent" } + paging.appendChild(countStr) + paging.appendChild(next) if (hasNext) { - paging.appendChild(next) + next.style.color="inherit" + } + else { + next.style.color="transparent" } while (pagingContainer.firstChild) { @@ -121,7 +139,7 @@ define([ return false } - currentEntityIdentifier = entityIdentifier.firstChild.value + currentEntityIdentifier = entityIdentifier.value if (!currentEntityIdentifier) { alert('Please specify an Entity identifier/permId') return false @@ -158,10 +176,11 @@ define([ return { - help: 'Download openBIS datasets to your local harddrive', + help: 'Download openBIS datasets', icon: 'fa-download', help_index: '', handler: function (env) { + console.log(env); state.selectedDatasets = new Set([]) conn_table = document.createElement("DIV") @@ -173,7 +192,13 @@ define([ showDataSets.appendChild(title) showDataSets.style.marginTop = '10px' - var entityIdentifier = entitySearcher.getEntitySearcherForDownload(state) + var entityIdentifier = document.createElement("INPUT") + entityIdentifier.type = "text" + entityIdentifier.name = "entityIdentifier" + entityIdentifier.size = 40 + entityIdentifier.placeholder = "Sample or Experiment identifier/permId" + entityIdentifier.value = state.entityIdentifier ? state.entityIdentifier : "" + var datasets_table = document.createElement("DIV") var pagingContainer = document.createElement("DIV") @@ -185,8 +210,9 @@ define([ showDataSets.appendChild(entityIdentifier) showDataSets.appendChild(show_datasets_btn) - showDataSets.appendChild(datasets_table) showDataSets.appendChild(pagingContainer) + showDataSets.appendChild(document.createElement('BR')) + showDataSets.appendChild(datasets_table) var dataset_direct = document.createElement("P") dataset_direct.style.marginTop = '10px' @@ -204,25 +230,39 @@ define([ downloadPath.type = "text" downloadPath.name = "downloadPath" downloadPath.size = "90" - downloadPath.value = state.workingDirectory + downloadPath.value = state.download_dir ? state.download_dir : state.working_dir - show_datasets_btn.onclick = + show_datasets_btn.onclick = () => getDatasets(env, 0, 5, entityIdentifier, datasets_table, pagingContainer, downloadPath) - + + var path = document.createElement("DIV") path.innerHTML = "<strong>download data to path: </strong>" path.appendChild(downloadPath) var download_dialog_box = document.createElement("DIV") + + let mainError = common.createErrorElement() + if (!state.connection.name) { + mainError.textContent = "Please choose a connection first." + //mainError.textContent = "Network problem: please check your connection first." + } + download_dialog_box.appendChild(mainError) download_dialog_box.appendChild(spinner) download_dialog_box.appendChild(conn_table) download_dialog_box.appendChild(showDataSets) download_dialog_box.appendChild(dataset_direct) download_dialog_box.appendChild(path) - function saveState() { + function onOk() { + state.entityIdentifier = entityIdentifier.value + state.directPermId = datasetPermId.value + state.download_dir = downloadPath.value + } + + function onCancel() { + state.entityIdentifier = entityIdentifier.value state.directPermId = datasetPermId.value - state.workingDirectory = downloadPath.value } function downloadDataset(connection_name, selectedPermIds, downloadPath) { @@ -252,7 +292,8 @@ define([ // keep current download path for later use currentDownloadPath = downloadPath }) - } else { + } + else { response.json() .then(function (error) { console.log(error.reason) @@ -292,7 +333,7 @@ define([ downloadDataset(selected_conn, selectedPermIds, downloadPath.value) showSpinner() - saveState() + onOk() return false } @@ -301,7 +342,7 @@ define([ title: 'Download openBIS DataSets', buttons: { 'Cancel': { - click: () => saveState() + click: () => onCancel() }, 'Download': { class: 'btn-primary btn-large', diff --git a/jupyter-openbis-extension/static/entitySearcher.js b/jupyter-openbis-extension/static/entitySearcher.js index 889db406d2be343c36739cf6107c6ed26a4c455a..70cf4c29922d9f1d07cc8f47e1b5dadd7bcc0124 100644 --- a/jupyter-openbis-extension/static/entitySearcher.js +++ b/jupyter-openbis-extension/static/entitySearcher.js @@ -64,24 +64,34 @@ define(["jquery", "./jquery-select2/js/select2.min"], element.innerHTML = "<span style='color:orange;margin:5px'>loading...</span>" if (!state.openbisService) { require.config(this.getRequireJSV3Config(state.connection.dto.url)) - require(["openbis", "as/dto/experiment/search/ExperimentSearchCriteria", - "as/dto/experiment/fetchoptions/ExperimentFetchOptions", - "as/dto/sample/search/SampleSearchCriteria", - "as/dto/sample/fetchoptions/SampleFetchOptions"], function(openbis) { + require(["openbis", "as/dto/experiment/search/ExperimentSearchCriteria", + "as/dto/experiment/fetchoptions/ExperimentFetchOptions", + "as/dto/sample/search/SampleSearchCriteria", + "as/dto/sample/fetchoptions/SampleFetchOptions"], + + function (openbis) { var apiUrl = state.connection.dto.url + "/openbis/openbis/rmi-application-server-v3.json" var v3 = new openbis(apiUrl) v3.login(state.connection.dto.username, state.connection.dto.password) - .done(function(sessionToken) { + .done(function (sessionToken) { + console.log("sessionToken heisst:"); + console.log(sessionToken); state.openbisService = v3 - _this.loadResource("/nbextensions/openbis/jquery-select2/css/select2.min.css", 'css', function() { - _this.createDropdown(element, state, upload) - }) - }).fail(function(result) { - alert('openbis v3 service login failed for ' + apiUrl - + " : property 'trusted-cross-origin-domains' is probably not set in service.properties.") + var userName = window.location.pathname.split("/")[2]; + //_this.loadResource('/user/' + userName+ '/nbextensions/jupyter-openbis-extension/jquery-select2/css/select2.min.css', 'css', function() { + _this.createDropdown(element, state, upload) + //}) }) + .fail(function (result) { + alert('openbis v3 service login failed for ' + apiUrl + + " : property 'trusted-cross-origin-domains' is probably not set in service.properties.") + }) + }, + function (err){ + alert('failed to load required libraries') }) - } else { + } + else { _this.createDropdown(element, state, upload) } diff --git a/jupyter-openbis-extension/static/state.js b/jupyter-openbis-extension/static/state.js index 71ea8e23d5a13982ed94c120171ef2e9674048d1..bcbe83fe121570577138ac2a503ac24d91bd5951 100644 --- a/jupyter-openbis-extension/static/state.js +++ b/jupyter-openbis-extension/static/state.js @@ -8,6 +8,9 @@ define([], candidateName: null, candidateDTO: null }, + working_dir: null, + working_dir_element: null, + download_dir: null, // upload dialog uploadDataSetType: null, @@ -21,7 +24,6 @@ define([], // download dialog selectedDatasets: new Set([]), entity: null, - workingDirectory: '', // openBIS v3 connection openbisService : null diff --git a/jupyter-openbis-extension/static/uploadDialog.js b/jupyter-openbis-extension/static/uploadDialog.js index 11cc7f651666265f1820a8f6b01bb53e82c09373..16e86669d0fbbee5cee2bafd112a66ea0b135591 100644 --- a/jupyter-openbis-extension/static/uploadDialog.js +++ b/jupyter-openbis-extension/static/uploadDialog.js @@ -24,23 +24,27 @@ define([ spinner.className="openbis-feedback" spinner.src="" function showSpinner(env) { - spinner.src = env.notebook.base_url + "nbextensions/openbis/spinner.gif" + var userName = window.location.pathname.split("/")[2]; + spinner.src = '/user/' + userName+ '/nbextensions/jupyter-openbis-extension/spinner.gif' } function hideSpinner(env) { spinner.src="" } - function get_file_list(env, container) { - var url = env.notebook.base_url + 'general/filelist' - + function get_file_list(env, container, path) { + // get the file list (relative to the notebook) using the existing jupyter api + var url = env.notebook.base_url + 'api/contents' + if (path !== "") { + url = url + '/' + path + } + fetch(url) .then( function(response) { if (response.ok) { response.json() .then(function(data){ - var values = Object.keys(data.files) - values.sort() - state.fileCheckboxes = createSelectTable(values, container, false, state.selectedFiles) + createFileTable(env, data, container, state) + //state.fileCheckboxes = createFileTable(env, data, container, state.selectedFiles) }) } else { @@ -173,7 +177,10 @@ define([ console.error("Error while parsing dataset types", error) }) - } else { + } + else { + // ! response.ok + errorElements.main.textContent = "Network problem: please check your connection first." while (dataset_types.firstChild) { dataset_types.removeChild(dataset_types.firstChild); } @@ -182,6 +189,7 @@ define([ .catch(function (error) { console.error("Error while fetching dataset types:", error) }) + } function createSelectTable(values, container, checked, overrides) { @@ -210,6 +218,96 @@ define([ return checkboxes } + function createFileTable(env, data, container, state) { + // list of files that will be uploaded as a dataSet. + + var table = document.createElement("TABLE") + table.className = 'table-bordered table-striped table-condensed' + table.style.width = "100%" + + var body = table.createTBody() + if (data.path !== "") { + var row = body.insertRow() + row.insertCell() + var iconCell = row.insertCell() + iconCell.className = "item_icon folder_icon icon-fixed-width" + var filenameCell = row.insertCell() + filenameCell.textContent = ".." + filenameCell.style.width = "80%" + filenameCell.style.cursor = "pointer" + filenameCell.onclick = function(){ + var elems = data.path.split('/') + elems.pop() + get_file_list(env, container, elems.join('/')) + } + + var sizeCell = row.insertCell() + sizeCell.style.textAlign = "right" + sizeCell.style.width = "15%" + } + + var registerFile = function () { + if (this.checked) { + // add file to state + state.selectedFiles.push(this.value) + } + else { + // remove file from state + state.selectedFiles.splice(state.selectedFiles.indexOf(this.value), 1) + } + } + + var checkboxes = [] + data.content.sort( (a, b) => a.name.localeCompare(b.name, undefined, {sensitivity: 'base'}) ).forEach( file => { + + var row = body.insertRow() + var checkboxCell = row.insertCell() + checkboxCell.style.width="5%" + var iconCell = row.insertCell() + var filenameCell = row.insertCell() + + filenameCell.textContent = file.name + filenameCell.style.width = "100%" + filenameCell.style.cursor = "pointer" + + if (file.type === "directory") { + iconCell.className = "item_icon folder_icon icon-fixed-width" + filenameCell.onclick = function () { + get_file_list(env, container, file.path) + } + } + else { + var checkbox = document.createElement("INPUT") + checkbox.type = "checkbox" + checkbox.value = file.path + checkbox.checked = state.selectedFiles.includes(file.path) ? true: false + checkbox.onclick = registerFile + checkboxes.push(checkbox) + checkboxCell.appendChild(checkbox) + + if (file.type === "notebook") { + iconCell.className = "item_icon notebook_icon icon-fixed-width" + } + else { + iconCell.className = "item_icon file_icon icon-fixed-width" + } + filenameCell.onclick = function () { + checkbox.checked = !checkbox.checked + registerFile.call(checkbox) + } + } + + var sizeCell = row.insertCell() + sizeCell.textContent = file.size + sizeCell.style.textAlign = "right" + sizeCell.style.width = "15%" + + }) + container.innerHTML = "" + container.appendChild(table) + return checkboxes + } + return { help: 'upload Notebook and Data to openBIS', icon: 'fa-upload', @@ -219,7 +317,7 @@ define([ var main_error = createErrorElement('main') var dst_title = document.createElement("STRONG") - dst_title.textContent = "DataSet type" + dst_title.textContent = "choose a dataSet type" var dataset_types = document.createElement("SELECT") dataset_types.id = "dataset_type" dataset_types.className = "form-control select-xs" @@ -232,13 +330,20 @@ define([ getDatasetTypes(env, state.connection.name, dataset_types, input_fields) var sample_title = document.createElement("STRONG") - sample_title.textContent = "Entity" + sample_title.textContent = "enter a sample/experiment identifier to attach this dataSet to:" var sample_error = createErrorElement('entityIdentifier') - var entityIdentifier = entitySearcher.getEntitySearcherForUpload(state) + //var entityIdentifier = entitySearcher.getEntitySearcherForUpload(state) + var entityIdentifier = document.createElement("INPUT") + entityIdentifier.type = "text" + entityIdentifier.name = "entityIdentifier" + entityIdentifier.size = 40 + entityIdentifier.placeholder = "Sample or Experiment identifier/permId" + entityIdentifier.value = state.entityIdentifier ? state.entityIdentifier : "" var ds_title = document.createElement("STRONG") + var dataSetListContainer = document.createElement("DIV") if (env.notebook.metadata.datasets) { ds_title.textContent = "Parent DataSets" @@ -248,11 +353,14 @@ define([ } var files_title = document.createElement("STRONG") - files_title.textContent = "Files" + files_title.textContent = "Files to upload" var fileListContainer = document.createElement("DIV") - fileListContainer.style.maxHeight="150px" + fileListContainer.style.height="200px" fileListContainer.style.overflow="auto" - get_file_list(env, fileListContainer) + // get the relative path + let re = new RegExp(env.notebook.notebook_name+"$") + rel_path = env.notebook.notebook_path.replace(re, "") + get_file_list(env, fileListContainer, rel_path) var inputs = document.createElement("DIV") inputs.style.marginTop = '10px' @@ -263,9 +371,12 @@ define([ inputs.appendChild(dataset_types) inputs.appendChild(input_fields) inputs.appendChild(sample_title) + inputs.appendChild(document.createElement('BR')) inputs.appendChild(sample_error) inputs.appendChild(entityIdentifier) + inputs.appendChild(document.createElement('BR')) inputs.appendChild(ds_title) + inputs.appendChild(document.createElement('BR')) inputs.appendChild(dataSetListContainer) inputs.appendChild(files_title) inputs.appendChild(fileListContainer) @@ -280,7 +391,7 @@ define([ } } state.unselectedDatasets = state.datasetCheckboxes.filter(cb => !cb.checked).map(cb => cb.value) - state.selectedFiles = state.fileCheckboxes.filter(cb => cb.checked).map(cb => cb.value) + //state.selectedFiles = state.fileCheckboxes.filter(cb => cb.checked).map(cb => cb.value) } function onOk() { @@ -293,12 +404,13 @@ define([ var uploadUrl = env.notebook.base_url + 'openbis/dataset/' + connection_name - var notebook = IPython.notebook - var files = state.fileCheckboxes.filter(cb => cb.checked).map(cb => cb.value) - var re = /\/notebooks\/(.*?)$/ - var filepath = window.location.pathname.match(re)[1] - files.push(filepath) - + // add this notebook to the list of files + var files = state.selectedFiles + if (! files.includes(env.notebook.notebook_path)) { + files.push(env.notebook.notebook_path) + } + console.log(files) + var props = {} for (input of $('#upload-input-fields').find('input')) { props[input.name] = input.value @@ -308,9 +420,10 @@ define([ "type": dataset_types.value, "files": files, "parents": state.datasetCheckboxes.filter(cb => cb.checked).map(cb => cb.value), - "entityIdentifier": entityIdentifier.firstChild.value, + "entityIdentifier": entityIdentifier.value, "props": props } + console.log(dataSetInfo); var settings = { url: uploadUrl, @@ -326,14 +439,14 @@ define([ common.createFeedback('success', data.statusText) // write statusText from returned data to notebooks metadata - if (typeof notebook.metadata.openbis === 'undefined') { - notebook.metadata.openbis = {} + if (typeof env.notebook.metadata.openbis === 'undefined') { + env.notebook.metadata.openbis = {} } - if (typeof notebook.metadata.openbis.permIds === 'undefined') { - notebook.metadata.openbis.permIds = {} + if (typeof env.notebook.metadata.openbis.permIds === 'undefined') { + env.notebook.metadata.openbis.permIds = {} } if (data.permId) { - notebook.metadata.openbis.permIds[data.permId] = data.statusText + env.notebook.metadata.openbis.permIds[data.permId] = data.statusText } }, error: function (data) { @@ -365,35 +478,43 @@ define([ saveState() return true } - - if (IPython.notebook.dirty === true) { - dialog.modal({ - body: 'Please save the notebook before uploading it to openBIS.', - title: 'Save notebook first', - buttons: { - 'Back': {} - }, - notebook: env.notebook, - keyboard_manager: env.notebook.keyboard_manager - }) - } else { - dialog.modal({ - body: uploadDialogBox, - title: 'Upload openBIS DataSet', - buttons: { - 'Cancel': { - click: onCancel - }, - 'Upload': { - class: 'btn-primary btn-large', - click: onOk - } + dialog_params = { + body: uploadDialogBox, + title: 'Upload openBIS DataSet', + buttons: { + 'Cancel': { + click: onCancel }, - notebook: env.notebook, - keyboard_manager: env.notebook.keyboard_manager - }) + 'Upload': { + class: 'btn-primary btn-large', + click: onOk + } + }, + notebook: env.notebook, + keyboard_manager: env.notebook.keyboard_manager + } + + if (env.notebook.dirty === true) { + env.notebook.save_notebook() + .then(function () { + dialog.modal(dialog_params) + }) + .catch(function (error) { + dialog.modal({ + body: error, + title: 'Error saving the notebook', + buttons: { + 'Back': {} + }, + notebook: env.notebook, + keyboard_manager: env.notebook.keyboard_manager + }) + }) + } + else { + dialog.modal(dialog_params) } } } } -) \ No newline at end of file +) diff --git a/setup.py b/setup.py index fb238db129b39ed313e0757152948972883c359e..dc56ee882d12e85791ccce9874f138aa3b2ee854 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ with open("README.md", "r", encoding="utf-8") as fh: setup( name='jupyter-openbis-extension', - version= '0.2.4', + version= '0.4.0', author='Swen Vermeul | ID SIS | ETH Zürich', author_email='swen@ethz.ch', description='Extension for Jupyter notebooks to connect to openBIS and download/upload datasets, inluding the notebook itself', @@ -23,7 +23,7 @@ setup( install_requires=[ 'jupyter-nbextensions-configurator', 'jupyter', - 'pybis>=1.8.4', + 'pybis>=1.9.5', 'numpy', 'tornado==5.1.1', ], @@ -43,18 +43,24 @@ setup( "jupyter-openbis-extension/static/connectionDialog.js", "jupyter-openbis-extension/static/uploadDialog.js", "jupyter-openbis-extension/static/spinner.gif", - "jupyter-openbis-extension/static/jquery-select2", - "jupyter-openbis-extension/static/jquery-select2/css", + "jupyter-openbis-extension/static/entitySearcher.js", + "jupyter-openbis-extension/static/main.js", + "jupyter-openbis-extension/static/state.js", + "jupyter-openbis-extension/static/common.js", + ]), + ("share/jupyter/nbextensions/jupyter-openbis-extension/jquery-select2/css", [ "jupyter-openbis-extension/static/jquery-select2/css/select2.min.css", "jupyter-openbis-extension/static/jquery-select2/css/select2-bootstrap.css", "jupyter-openbis-extension/static/jquery-select2/css/select2-bootstrap.min.css", "jupyter-openbis-extension/static/jquery-select2/css/select2.css", - "jupyter-openbis-extension/static/jquery-select2/js", + ]), + ("share/jupyter/nbextensions/jupyter-openbis-extension/jquery-select2/js", [ "jupyter-openbis-extension/static/jquery-select2/js/select2.min.js", "jupyter-openbis-extension/static/jquery-select2/js/select2.full.min.js", "jupyter-openbis-extension/static/jquery-select2/js/select2.js", "jupyter-openbis-extension/static/jquery-select2/js/select2.full.js", - "jupyter-openbis-extension/static/jquery-select2/js/i18n", + ]), + ("share/jupyter/nbextensions/jupyter-openbis-extension/jquery-select2/js/i18n", [ "jupyter-openbis-extension/static/jquery-select2/js/i18n/pt.js", "jupyter-openbis-extension/static/jquery-select2/js/i18n/vi.js", "jupyter-openbis-extension/static/jquery-select2/js/i18n/lv.js", @@ -102,12 +108,6 @@ setup( "jupyter-openbis-extension/static/jquery-select2/js/i18n/es.js", "jupyter-openbis-extension/static/jquery-select2/js/i18n/ro.js", "jupyter-openbis-extension/static/jquery-select2/js/i18n/tr.js", - "jupyter-openbis-extension/static/jquery-select2/VERSION.txt", - "jupyter-openbis-extension/static/jquery-select2/LICENSE.txt", - "jupyter-openbis-extension/static/entitySearcher.js", - "jupyter-openbis-extension/static/main.js", - "jupyter-openbis-extension/static/state.js", - "jupyter-openbis-extension/static/common.js", ]), # like `jupyter nbextension enable --sys-prefix` ("etc/jupyter/nbconfig/notebook.d", [