diff --git a/.eslintrc.yml b/.eslintrc.yml index c0442c4006..257496302f 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -3,7 +3,7 @@ env: amd: true extends: 'eslint:recommended' parserOptions: - ecmaVersion: 5 + ecmaVersion: 6 rules: strict: - error diff --git a/.travis.yml b/.travis.yml index faedee21d9..4de9861ba3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,4 @@ -dist: trusty +dist: bionic sudo: required language: python python: @@ -45,10 +45,10 @@ install: - conda create -q -n test-environment python=$TRAVIS_PYTHON_VERSION - source activate test-environment -before_script: - - "export DISPLAY=:99.0" - - "sh -e /etc/init.d/xvfb start" - - sleep 3 # give xvfb some time to start +# before_script: +# - "export DISPLAY=:99.0" +# - "sh -e /etc/init.d/xvfb start" +# - sleep 3 # give xvfb some time to start script: - make build-travis-narrative diff --git a/Makefile b/Makefile index 56bd9bf4b5..624f399260 100755 --- a/Makefile +++ b/Makefile @@ -3,7 +3,6 @@ REPO_NAME = narrative # Installer script INSTALLER = ./scripts/install_narrative.sh -INSTALL_VENV = narrative-venv BACKEND_TEST_SCRIPT = scripts/narrative_backend_tests.sh FRONTEND_TEST_DIR = test @@ -18,12 +17,14 @@ build-narrative-container: docker_image: build-narrative-container # Per PR #1328, adding an option to skip minification -dev_image: +dev-image: SKIP_MINIFY=1 DOCKER_TAG=dev sh $(DOCKER_INSTALLER) +run-dev-image: + ENV=$(ENV) bash scripts/local-dev-run.sh + install: - @echo "Installing local Narrative in the $(INSTALL_VENV) virtual environment" - bash $(INSTALLER) -v $(INSTALL_VENV) + bash $(INSTALLER) # runs the installer to locally build the Narrative in a # local venv. diff --git a/README.md b/README.md index 8889fa5987..d039600485 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,17 @@ conda activate my_narrative_environment ./scripts/install_narrative.sh kbase-narrative ``` +If the previous instructions do not work, try +``` +# source ~/anaconda3/bin/activate or wherever you have python installed +conda create -n my_narrative_environment +conda activate my_narrative_environment +sh scripts/install_narrative.sh +# scripts/install_narrative.sh +kbase-narrative +``` + + ### Or, without conda - this installs lots of requirements of specific versions and may clobber things on your PYTHONPATH. ``` @@ -81,6 +92,16 @@ To run authenticated tests, you'll need to get an auth token from KBase servers, Note: **DO NOT CHECK YOUR TOKEN FILE IN TO GITHUB**. You'll be shamed without mercy. +## Manual Testing + +* It can be useful to immediately see your changes in the narrative. For javascript changes, you will just have to reload the page. You can print messages to the console with `console.log` + +* For python changes, it will require shutting down the notebook, running `scripts/install_narrative.sh -u` and then starting the notebook server up again with kbase-narrative. You can print messages to the terminal using + +``` +log = logging.getLogger("tornado.application") +log.info("Your Logs Go Here") +``` ## Submitting code diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index f0b0811128..59e06b93e9 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -3,8 +3,17 @@ The Narrative Interface allows users to craft KBase Narratives using a combinati This is built on the Jupyter Notebook v6.0.2 (more notes will follow). +### Version 4.2.0 +- Updated the Narrative interface to connect to the remade Execution Engine. +- Updated the Narrative interface to streamline events and cookies connected to the Traefik update. +- Fixed an issue where job log browser state (running, stopped, scrolling) could cross browser sessions. +- Added a viewer for the SampleSet object. +- PTV-1446 - fix bug preventing KBaseFeatureValues viewer apps from working and displaying data + ### Version 4.1.2 -- Redirect to the interstitial loading page after shutting down a Narrative session, instead of letting the backend server do the redirect. +- Improve display of job logs. +- Prevent App cell elements from overflowing the page. +- Job status is now inaccessible before a job enters the queue. ### Version 4.1.1 - Fix sort order in Narratives panel - should be by most recently saved. diff --git a/bower.json b/bower.json index a8d7d13bc2..7e795e86d1 100644 --- a/bower.json +++ b/bower.json @@ -1,6 +1,6 @@ { "name": "kbase-narrative", - "version": "4.1.2", + "version": "4.2.0", "homepage": "https://kbase.us", "dependencies": { "bluebird": "3.4.7", diff --git a/docs/design/app-cells/about.md b/docs/design/app-cells/about.md index e69de29bb2..7ba2b4f2fd 100644 --- a/docs/design/app-cells/about.md +++ b/docs/design/app-cells/about.md @@ -0,0 +1,20 @@ +# Flow and Modules + +## 1. nbextensions +### Jupyter Notebook extensions +< blurb about Jupyter Notebook extensions here and what we're doing > + +### App Cell extension pattern +Detection +Initialization +Cell transformation + +## 2. App Cell Widget as controller +Main controller +Finite State Machine + +## 3. App Cell Tabs + +### A. Configure Tab + +### B. Job Status Tab diff --git a/docs/design/job_architecture.md b/docs/design/job_architecture.md index 026fbeb017..6c9430cad9 100644 --- a/docs/design/job_architecture.md +++ b/docs/design/job_architecture.md @@ -1,158 +1,506 @@ # Job Management Architecture +The Narrative job manager is based on a data flow that operates between the browser, the IPython Kernel behind the Narrative, and the KBase Execution Engine (EE2) behind that. In general, each bit of data flows between those three stops. + +In general, there's a single point of information flow on the front end and one on the backend. + +# Comm channels +Jupyter provides a "Comm" object that allows for custom messaging between the frontend and the kernel ([details and documentation here](https://jupyter-notebook.readthedocs.io/en/stable/comms.html)). This provides an interface for the frontend to directly request information from the kernel, and to listen to asynchronous responses. On the kernel-side, it allows one or more modules to register message handlers to process those requests out of the band of the usual kernel invocation. These are used to implement the Jupyter Notebook's ipywidgets, for example. + +The Narrative Interface uses one of these channels to manage job information. These are funneled through an interface on the frontend side and a matching one in the kernel. + +## Frontend Comm Channel +On the frontend, there's a `jobCommChannel.js` module that uses the MiniBus system to communicate. So, the following happens. Frontend modules use the bus system to send one of the following messages over the main channel, which then get interpreted and crafted into a message that gets passed through a kernel comm object. Most of these take one or more inputs. These are listed below the command, where applicable. + +This section is broken into two parts - kernel requests and kernel responses. Both of these are from the perspective of the frontend Javascript stack, using an AMD module and the Runtime object. The request parameters and examples are given first, then the responses below. + +## Bus requests +These messages are sent to the `JobCommChannel` on the front end, to get processed into messages sent to the kernel. +`ping-comm-channel` - sees that the comm channel is open through the websocket + +`request-job-status` - gets the status for a job, one time + * `jobId` - a string, the job id + * `parentJobId` - (optional) a string, the id of the requested job's "parent" job + +`request-job-update` - request the status for a job, but start an update cycle so that it's continually requested. + * `jobId` - a string, the job id + * `parentJobId` - (optional) a string, the id of the requested job's "parent" job + +`request-job-completion` - signal that the front end doesn't need any more updates for that job, so stop sending them for each loop cycle. Doesn't actually end the job, only requests for updates. + * `jobId` - a string, the job id + * `parentJobId` - (optional) a string, the id of the requested job's "parent" job + +`request-job-info` - request information about the job, specifically app id, spec, input parameters and (if finished) outputs + * `jobId` - a string, the job id + * `parentJobId` - (optional) a string, the id of the requested job's "parent" job + +`request-job-cancellation` - request that the server cancel the running job. + * `jobId` - a string, the job id + * `parentJobId` - (optional) a string, the id of the requested job's "parent" job + +`request-job-log` - request the job logs starting at some given line. + * `jobId` - a string, the job id + * `options` - an object, with attributes: + * `first_line` - the first line (0-indexed) to request + * `num_lines` - the number of lines to request (will get back up to that many if there aren't more) + +`request-latest-job-log` - request the latest several job log lines + * `jobId` - a string, the job id + * `options` - an object, with attributes: + * `num_lines` - the number of lines to request (will get back up to that many if there aren't more) + +### Usage Example +The comm channel is used through the main Bus object that's instantiated through the global `Runtime` object. That needs to be included in the `define` statement for all AMD modules. The bus is then used with its `emit` function (you have the bus *emit* a message to its listeners), and any inputs are passed along with it. + +Generally, this is used as follows (without much detail. For a readable real example, check out the `jobLogViewer.js` module): + +```Javascript +define( + ['common/runtime', ... other modules ...], + function(Runtime, ...others...) { + let runtime = Runtime.make(); + runtime.bus().emit('some-request', { + inputKey: 'value' + }); + } +); +``` + +Or, a more specific usage that requests the first 10 job log lines: +```Javascript +define( + ['common/runtime'], + function(Runtime) { + let runtime = Runtime.make(); + runtime.bus().emit('request-job-log', { + jobId: 'some_job_id', + options: { + first_line: 0, + num_lines: 10 + } + }); + } +); +``` + +## Bus responses +When the kernel sends a message to the front end, the only module set up to listen to them is the `JobCommChannel` as mentioned above. This takes the responses, unpacks them, and turns them into a response message that is passed back over the bus to any frontend Javascript module that listens to them. The message types are described below, along with the content that gets sent, followed by an example of how to make use of them. + +`job-status` - contains the current job state + * `jobId` - string, the job id + * `jobState` - object, describes the job state (see the **Data Structures** section below for the structure) + * `outputWidgetInfo` - object, contains the parameters to be sent to an output widget. This will be different for all widgets, depending on the App that invokes them. + +`job-deleted` - sent when a job has been deleted, but some information about it has been requested + * `jobId` - the id of the deleted job + * `via` - a string about why it's been deleted (generally "no_longer_exists") + +`job-info` - contains information about the current job + * `jobId` - string, the job id + * `jobInfo` - object, the job information object (see the **Data Structures** section below) + +`run-status` - updates the run status of the job - this is part of the initial flow of starting a job through the AppManager. + * TODO + +`job-canceled` - sent when a job has been canceled in the kernel, as a response to other messages + * `jobId` - string, the job id + * `via` - string, generally "job_canceled" + +`job-logs` - sent with information about some job logs. + * `jobId` - string, the job id + * `logs` - the raw message data from the kernel. (see the **Data Structures** section below) + * `latest` - if truthy, then these are the latest logs, if falsy, then they don't have to be the latest logs. + +`job-error` - sent in response to an error that happened on job information lookup, or another error that happened while processing some other message to the JobManager. + * `jobId` - string, the job id + * `message` - string, some message about the error + +`job-cancel-error` - a cancel request has thrown an error + * `jobId` - string, the job id + * `message` - string, a reason for the error + +`job-log-deleted` - a log request has thrown an error + * `jobId` - string, the job id + * `message` - string, a reason for the error + +`job-status-error` - a status request as thrown an error + * `jobId` - string, the job id + * `message` - string, a reason for the error + +`job-does-not-exist` - sent in response to a request for information about a job that doesn't exist. Jobs might not exist if (1) they have been previously canceled, or (2) a malformed request was sent. + * `jobId` - string, the job id + * `source` - string, the source of the message in the kernel (what service, or module, was invoked. Usually "JobManager" or "ExecutionEngine2") + +### Usage example +As in the Bus requests section above, the front end response handling is done through the Runtime bus. The bus provides both an `on` and a `listen` function, examples will show how to use both. Generally, the `listen` function is more specific and binds the listener to a specific bus channel. These channels can invoke the jobId, or the cellId, to make sure that only information about specific jobs is listened for. + +The `listen` function takes an object with three attributes as input - a channel (either the cellId or jobId), a key (with the type of message to listen for), and a handle, which is a function to process the message. This is probably the easiest way to handle messages. A usage would look like this: + +```Javascript +define(['common/runtime'], + function(Runtime) { + let runtime = Runtime.make(); + let listenerId = runtime.bus().listen({ + channel: { + jobId: 'some_job_id' + }, + key: { + type: 'job-status' + }, + handle: (message) => { + ...process the message... + } + }) + } +); +``` + +The `on` function requires a constructed channel bus, premade and reusable for a given channel. So you would make a channel bus that would always receive messages for that channel, and instruct it on what to do when a message of a given type arrives. That looks like this: +```Javascript +define(['common/runtime'], + function(Runtime) { + let runtime = Runtime.make(); + let cellBus = runtime.bus().makeChannelBus({ + name: { + cell: 'some_cell_id' + } + }); + let listenerId = cellBus.on('run-status', (message) => { + ...process the message... + }); + } +); +``` +Note that both of these create events that get bound to the DOM, and when the widget is removed, they should be cleaned up. This can be done by calling `bus.removeListener(id)` with the created `listenerId`. If you created a channel bus, then that bus should be used, otherwise the main runtime.bus() object should be used. + +## Kernel Comm Channel +On the kernel side, a complementary comm channel is used. This is set up in the `biokbase.narrative.jobs.jobcomm.JobComm` class. On Narrative load, page reload, or kernel restart, this is initialized to handle any messages sent to the kernel. The structure here is slightly different than the structure used on the front end. Likewise, all the message names are different. They all have a request string, most involve a job id, and that's it. The job logs request also have which line to start with and how many lines to get back. + +Note that these are autogenerated by the frontend `JobCommChannel` object, using the `Jupyter.kernel.comm` package. + +The actual message that the JobComm sees in the kernel has this format: +``` +{ + "msg_id": "some random string", + "content": { + "data": { + "request_type": "a string - see below", + "job_id": "not required, but present in most" + ... other keys, depending on message ... + } + } +} +``` +The point here is that all messages have a `request_type`, most are accompanied by a `job_id`, and a few have some extra info. But they're in a flat structure that's formatted by the Jupyter kernel. + +## Messages sent to the kernel +These are organized by the `request_type` field, followed by the expected response message. Additional parameters and their formats are given as a list below the request name. E.g. the `job_status` message will be sent as: +```json +{ + "msg_id": "some string", + "content": { + "data": { + "request_type": "job_status", + "job_id": "a_job_id", + "parent_job_id": "another_job_id" + } + } +} +``` + +`all_status` - request the status of all currently running jobs, responds with `job_status_all` + +`job_status` - request a single job status, responds with `job_status` +* `job_id` - string, +* `parent_job_id` - optional string + +`start_update_loop` - request starting the global job status update thread, no specific response, but generally with `job_status_all` + +`stop_update_loop` - request stopping the global job status update thread, no response + +`start_job_update` - request updating a single job during the update thread, no specific response, but generally with `job_status` +* `job_id` - string +* `parent_job_id` - optional string + +`stop_job_update` - request halting update for a single job during the update thread, no response +* `job_id` - string +* `parent_job_id` - optional string + +`job_info` - request general information about a job, responds with `job_info` +* `job_id` - string +* `parent_job_id` - optional string + +`job_logs` - request job log information, responds with `job_logs` +* `job_id` - string +* `parent_job_id` - optional string +* `first_line` - int >= 0, +* `num_lines` - int > 0 + +`job_logs_latest` - request the latest set of lines from job logs, responds with `job_logs` +* `job_id` - string +* `parent_job_id` - optional string +* `num_lines` - int > 0 + + +## Messages sent from the kernel to the browser +These are all caught by the `JobCommChannel` on the browser side, then parsed and sent as the bus messages described above. Like other kernel messages, they have a `msg_type` field, and a `content` field containing data meant for the frontend to use. They have a rough structure like this: + +```json +{ + "data": { + "msg_type": "some_message", + "content": { + "key1": "value1", + "key2": "value2" + } + } +} +``` + +a specific example: +```json +{ + "msg_id": "some_string", + "data": { + "msg_type": "job_status", + "content": { + "state": { + "status": "running", + ... other state keys ... + }, + "spec": {}, + "widget_info": {} + } + } +} +``` + +These are described below. The name (`msg_type`) is given, followed by the keys given in the `content` block. + +By design, these should only be seen by the `JobCommChannel` instance, then sent into bus messages that get sent on specific channels. That information is also given in each block. + +### `job_does_not_exist` +This is an error message triggered when trying to get info/state/logs on a job that either doesn't exist in EE2 or that the JobManager doesn't have associated with the running narrative. + +**content** + * `job_id` - a string, the job id + * `source` - string, the source of the error + +**bus** `job-does-not-exist` + +### `job_comm_error` +A general job comm error, capturing most errors that get thrown by the kernel + +**content** (this varies, but usually includes the below) + * `request_type` - the original request message that wound up in an error + * `job_id` - string, the job id (if present) + * `message` - string, an error message + +**bus** one of `job-cancel-error`, `job-log-deleted`, `job-status-error`, `job-error` + +### `job_status_all` +The set of all job states for all running jobs, or at least the set that should be updated (those that are complete and not requested by the front end are not included - if a job is sitting in an error or finished state, it doesn't need ot have its app cell updated) + +**content** - all of the below are included, but the top-level keys are all job id strings, e.g.: +```json +{ + "job_id_1": { ...contents... }, + "job_id_2": { ...contents... } +} +``` + * `state` - the job state (see the **Data Structures** section below for details + * `widget_info` - the parameters to send to output widgets, only available for a completed job + * `owner` - string, username of user who submitted the job + +**bus** - a series of `job-status` or `job-deleted` messages + +### `job_info` +Includes information about the running job + +**content** + * `app_id` - string, the app id (format = `module_name/app_name`) + * `app_name` - string, the human-readable app name + * `job_id` - string, the job id + * `job_params` - the unstructured set of parameters sent to the execution engine + +**bus** - `job-info` + +### `job_status` +The current job state. This one is probably most common. + +**content** + * `state` - see **Data Structures** below for details (it's big and shouldn't be repeated all over this document) + * `widget_info` - the parameters to send to output widgets, only available for a completed job + * `owner` - string, username of user who submitted the job + +**bus** - `job-status` + +### `job_logs` +Includes log statement information for a given job. + +**content** + * `job_id` - string, the job id + * `latest` - boolean, `true` if this is just the latest set of logs + * `first` - int, the index of first line included in the set + * `max_lines` - int, the total log lines available in the server + * `lines` - list of log line objects, each one has the following keys: + * `line` - string, the log line + * `is_error` - 0 or 1, if 1 then the line is an "error" as reported by the server + +**bus** `job-logs` + +### `new_job` +Sent when a new job is launched and serialized. This just triggers a save/checkpoint on the frontend - no other bus message is sent + +### `run_status` +Sent during the job startup process. There are a few of these containing various startup status, including errors (if they happen). + +**content** +All cases: + * `event` - string, what's the run status + * `event_at` - string, timestamp + * `cell_id` - the app cell id (used for routing) + * `run_id` - the run id of the app (autogenerated by the cell) + +(if error) + * `event` - string, "error", + * `event_at` - string, timestamp + * `error_message` - string, the error + * `error_type` - string, the type of Exception that was raised. + * `error_stacktrace` - string, a stacktrace + * `error_code` - int, an error code + * `error_source` - string, the "source" of the error (generally "appmanager") + +(if ok) + * `job_id` - if the job was launched successfully + +**bus** `run-status` + +### `result` +Sent at the end of a `AppManager.run_dynamic_service` call (of which there aren't many). + +**content** + * `cell_id` - the app cell id (used for routing) + * `run_id` - the run id of the app (autogenerated by the cell) + * `event_at` - string, timestamp + * `result` - the result of the dynamic service call (some unspecified object) + +**bus** `result` - sent to `cell_id` channel ## Job Management flow on backend (in IPython kernel, biokbase.narrative.jobs package) -This is mostly linear. +These steps define the process of creating a new app running job. 1. User clicks "Run" on App Cell in browser. * Cell provides app_id, cell_id, run_id, and parameters. * Invokes biokbase.narrative.appmanager.AppManager.run_app. -2. `AppManager.run_app` validation: +2. `AppManager.run_app` validates the following bits of information before passing them on to EE2: * App (based on id, version, and spec). - * Parameters. + * Parameters (based on the app spec). 3. `AppManager.run_app` preparation and start: - * Convert app params from user-readable to machine-understandable (via spec input_mapping). + * Convert app params from user-readable to machine-understandable (via the spec input_mapping). * Fetch cell id, run id, workspace id, user token. - * Create an agent token on behalf of the user. + * Create an agent token on behalf of the user. This effectively makes a new authentication token that has a two-week lifetime, separate from the current login token. For example, if the user's current login token has a remaining lifespan of 1 hour, then the new job will be able to continue long past that. * Submit all of the above to `NarrativeJobService.run_job`. 4. Get response from `NarrativeJobService.run_job` * Combine with info from step 3, create `biokbase.narrative.jobs.job.Job` object. * submit new `Job` to `biokbase.narrative.jobs.jobmanager.JobManager` singleton object. -5. `JobManager` fetches job status and pushes it to browser. +5. `AppManager` tells the `JobComm` channel to (1) fetch the new job status and push it to the browser, and (2) start the job lookup loop for the newly created job. (calls `AppManager.register_new_job`) -JobManager initialization and startup. +## JobManager initialization and startup. +These steps take place whenever the user loads a narrative, or when the kernel is restarted. This ensures that the JobManager in the kernel is kept up-to-date on Job status. 1. User starts kernel (opens a Narrative, or clicks Kernel -> Restart) -2. `kbaseNarrativeJobPanel` (invisible front end widget) executes kernel call to `JobManager().initialize_jobs()` +2. `jobCommsChannel` (front end widget) executes the following kernel call: `JobManager().initialize_jobs(); JobComm().start_update_loop` 3. `JobManager` does: * Get current user and workspace id. - * `UserAndJobState.list_jobs2` with the workspace id - gets the list of jobs in that workspace - * `NarrativeJobService.check_jobs` with the list of job ids, and request for job params, esp. metadata. - * Call `_lookup_all_job_status` to push status of all jobs forward to front end. - * (optional) Call `_lookup_job_status_loop` to start a lookup thread (uses a Python `Timer` to lookup job status) - -JobManager status lookup loop. -1. Calls either `_lookup_all_job_status` or `_lookup_job_status_loop` (which itself calls the former) -2. Build a list of job ids to lookup - those that are flagged for lookup. -3. Calls `_construct_job_status_set` - * Calls `_get_all_job_states` + * `ExecutionEngine2.check_workspace_jobs` with the workspace id - gets the set of jobs in that workspace, and builds them into `Job` objects. +4. `JobComm` does: + * Starts the lookup loop thread. + * On the first pass, this looks up status of all jobs and pushes them forward to the browser. + * If any jobs are in a terminal state, they'll stopped being looked up automatically. If all jobs are terminated, then the loop thread itself stops. + +## JobComm status lookup loop. +1. Calls `JobComm._lookup_job_status_loop`, which in turn calls `JobComm.lookup_all_job_states`. This gets forwarded to `JobManager.lookup_all_job_states`, and the results pushed to the browser as a comm channel message. +2. Internal to `JobManager.lookup_all_job_states`, the following steps happen: + * Build a list of job ids to lookup - those that are flagged for lookup. + * Call `_construct_job_status_set` + * Call `_get_all_job_states` * Gets list of all job ids the JM is tracking. * Retrieves some states from cache (cache is used for finalized jobs). * Calls `NarrativeJobService.check_jobs` on everything that's not finalized. * Injects `run_id` and `cell_id` into states * Returns dict of states. -4. Sends result to browser over comm channel - +3. Sends result to browser over comm channel +4. Since this runs in the background on the kernel-side, it removes any need for the browser to constantly poll. ## Data Structures ### Job state -In kernel, as retrieved from NJS.check_job +In kernel, as retrieved from EE2.check_job (described by example) ```json { - "status": [ - "2019-05-07T22:42:41+0000", - "started", - "queued", - null, - null, - 0, - 0 - ], - "job_id": "5cd209dcaa5a4d298c5dc1c2", - "job_state": "queued", - "creation_time": 1557268961909, - "ujs_url": "https://ci.kbase.us/services/userandjobstate/", - "finished": 0, - "sub_jobs": [], - "awe_job_state": "queued" + "user": "wjriehl", + "authstrat": "kbaseworkspace", + "wsid": 46214, + "status": "queued", + "updated": 1583863267977, + "queued": 1583863267977, + "scheduler_type": "condor", + "scheduler_id": "14221", + "job_input": { + "wsid": 46214, + "method": "simpleapp.simple_add", + "params": [ + { + "workspace_name": "wjriehl:narrative_1580237536246", + "base_number": 5 + } + ], + "service_ver": "f5a7586776c31b05ae3cc6923c2d46c25990d20a", + "app_id": "simpleapp/example_method", + "source_ws_objects": [], + "parent_job_id": "None", + "requirements": { + "clientgroup": "njs", + "cpu": 4, + "memory": 23000, + "disk": 100 + }, + "narrative_cell_info": { + "run_id": "d7558838-a712-42d3-9511-c4b95f3651fe", + "token_id": "e80f4f81-b7bb-4483-a92b-b1e0200f8a20", + "tag": "beta", + "cell_id": "c04c19bd-20ce-41be-b793-50f84de8f60b" + } + }, + "job_id": "5e67d5e395d1f00a7cf4ea21", + "created": 1583863267000 } ``` As sent to browser, includes cell info and run info ``` { - owner: string (username), + owner: string (username, who started the job), spec: app spec (optional) widget_info: (if not finished, None, else...) job.get_viewer_params result state: { - job_state: string, - error (if present): dict of error info, - cell_id: string/None, - run_id: string/None, - awe_job_id: string/None, - canceled: 0/1 - creation_time: epoch second - exec_start_time: epoch/none, - finish_time: epoch/none, - finished: 0/1, job_id: string, - status: (from UJS) [ - timestamp(last_update, string), - stage (string), - status (string), - progress (string/None), - est_complete (string/None), - complete (0/1), - error (0/1) - ], - ujs_url: string + status: string, + created: epoch ms, + updated: epoch ms, + queued: optional - epoch ms, + finished: optional - epoch ms, + terminated_code: optional - int, + tag: string (release, beta, dev), + parent_job_id: optional - string or null, + run_id: string, + cell_id: string, + errormsg: optional - string, + error (optional): { + code: int, + name: string, + message: string (should be for the user to read), + error: string, (likely a stacktrace) + }, + error_code: optional - int } } ``` - - -## Application, execution cycle notes: - -User clicks App Panel -> App Cell inserted. Done! - -User clicks "Run" on App Cell (or otherwise executes a function with a cell_id stuck to it) -> -App Cell executes code (should disable the Run button) -> -Kernel goes through AppManager.run_app steps: - sends over comm channel: - 1. run_status with serialized events: - a. validating_app - b. validated_app - c. launching_job - d. launched_job - 2. job_status - 3. new_job (empty, triggers a save) - 4. run_app_error (on NJSW.run_job failure) --> -Job Panel catches these and translates messages before sending out over Bus: - run_status -> run-status - job_status -> job-status - new_job -> null - run_app_error -> ... nothing? Should get a message. - -App Cell catching messages: -run-status -> updates FSM from launching..... launched Job (with an id in job-status), now listens to jobId channel -job-status -> updates copy of job state, elapsed time, display of state -job-status (with terminal status) -> updates job state, expectes no more changes, creates output cell and area - -JobManager kernel loop: - loops over all jobs, gets state, sends job_status for all - sends job_err for individual jobs if - 1. job is missing (e.g. JobManager maintains a handle, but job.state() fails because NJS can't find it - 2. Network error - JobPanel translates messages - -Job Canceling: -Permissions! - View permissions - no permission to touch jobs - Edit permissions - can cancel jobs, start jobs, delete own started job, not delete other's started jobs - Admin - can cancel, delete, view, start - -User cancels job - either in App Cell Cancel button or Job Panel Cancel button - sends cancel-job to JobPanel - sends cancel_job to kernel - JobManager tries to cancel the job - sends job_canceled if successful (or already canceled) - sends job_err if not - with sensible reason - -User deletes job - either in App Cell or Job Panel - sends delete-job to JobPanel - sends delete_job to kernel - JobManager tries to cancel the job (if not in a completed state): - sends job_canceled if successful - sends job_err if not - JobManager tries to delete the job: - sends job_deleted if successful - sends job_err if not diff --git a/kbase-extension/static/ext_packages/jquery-extensions/js/jquery.cookie.min.js b/kbase-extension/static/ext_packages/jquery-extensions/js/jquery.cookie.min.js deleted file mode 100644 index 7401208d4e..0000000000 --- a/kbase-extension/static/ext_packages/jquery-extensions/js/jquery.cookie.min.js +++ /dev/null @@ -1,8 +0,0 @@ -/*! - * jQuery Cookie Plugin v1.3.1 - * https://github.com/carhartl/jquery-cookie - * - * Copyright 2013 Klaus Hartl - * Released under the MIT license - */ -(function(a,b,c){function e(a){return a}function f(a){return g(decodeURIComponent(a.replace(d," ")))}function g(a){return 0===a.indexOf('"')&&(a=a.slice(1,-1).replace(/\\"/g,'"').replace(/\\\\/g,"\\")),a}function h(a){return i.json?JSON.parse(a):a}var d=/\+/g,i=a.cookie=function(d,g,j){if(g!==c){if(j=a.extend({},i.defaults,j),null===g&&(j.expires=-1),"number"==typeof j.expires){var k=j.expires,l=j.expires=new Date;l.setDate(l.getDate()+k)}return g=i.json?JSON.stringify(g):g+"",b.cookie=[encodeURIComponent(d),"=",i.raw?g:encodeURIComponent(g),j.expires?"; expires="+j.expires.toUTCString():"",j.path?"; path="+j.path:"",j.domain?"; domain="+j.domain:"",j.secure?"; secure":""].join("")}for(var m=i.raw?e:f,n=b.cookie.split("; "),o=d?null:{},p=0,q=n.length;q>p;p++){var r=n[p].split("="),s=m(r.shift()),t=m(r.join("="));if(d&&d===s){o=h(t);break}d||(o[s]=h(t))}return o};i.defaults={},a.removeCookie=function(b,c){return null!==a.cookie(b)?(a.cookie(b,null,c),!0):!1}})(jQuery,document); \ No newline at end of file diff --git a/kbase-extension/static/kbase/css/kbaseJobLog.css b/kbase-extension/static/kbase/css/kbaseJobLog.css index 3da3e65501..e217362f96 100644 --- a/kbase-extension/static/kbase/css/kbaseJobLog.css +++ b/kbase-extension/static/kbase/css/kbaseJobLog.css @@ -16,7 +16,6 @@ .kblog-header { display: flex; font-family: monospace; - font-size: 85%; } .kblog-line { @@ -50,6 +49,7 @@ .kblog-text { word-wrap: break-word; + overflow-wrap: break-word; flex: 1; margin-left: 6px; } diff --git a/kbase-extension/static/kbase/js/api/auth.js b/kbase-extension/static/kbase/js/api/auth.js index 264b9bfcaa..37f62d9f81 100644 --- a/kbase-extension/static/kbase/js/api/auth.js +++ b/kbase-extension/static/kbase/js/api/auth.js @@ -1,14 +1,128 @@ define([ 'bluebird', 'jquery', - 'util/string', - 'jqueryCookie' -], function(Promise, $, StringUtil) { + 'narrativeConfig' +], function ( + Promise, + $, + Config +) { 'use strict'; function factory(config) { - var url = config.url; - var cookieName = 'kbase_session'; + const url = config.url; + const secureCookies = typeof config.secureCookies === 'undefined' ? true : config.secureCookies; + + /* + Each cookie is defined + */ + const cookieConfig = { + auth: { + name: 'kbase_session' + }, + backup: { + name: 'kbase_session_backup', + domain: 'kbase.us', + enableIn: ['prod'] + }, + narrativeSession: { + name: 'narrative_session' + } + }; + + const TOKEN_AGE = 14; // days + + /** + * Meant for managing auth or session cookies (mainly auth cookies as set by + * a developer working locally - which is why this is very very simple). + * Get a cookie "object" (key-value pairs) as input. + * If it's missing name or value, does nothing. + * Default expiration time is 14 days. + * domain, expires, and max-age are optional + * expires is expected to be in days + * auto set fields are: + * - path = '/' + * - expires = TOKEN_AGE (default 14) days + * @param {object} cookie + * - has the cookie keys: name, value, path, expires, max-age, domain + * - adds secure=true, samesite=none for KBase use. + */ + function setCookie(cookie) { + if (!cookie.name) { + return; + } + const name = encodeURIComponent(cookie.name); + const value = encodeURIComponent(cookie.value || ''); + const props = { + expires: TOKEN_AGE, // gets translated to GMT string + path: '/', + samesite: 'none' + }; + if (Number.isInteger(cookie.expires)) { + props.expires = cookie.expires; + } + + // Default to secure cookies global setting if not specified. + if (typeof cookie.secure === 'undefined') { + cookie.secure = secureCookies; + } + + if (cookie.domain) { + props.domain = cookie.domain; + } + props['max-age'] = 86400 * props.expires; + if (props.expires === 0) { + props.expires = new Date(0).toUTCString(); + } else { + props.expires = new Date(new Date().getTime() + (86400000*props.expires)).toUTCString(); + } + + const fields = Object.keys(props).map((key) => { + return `${key}=${props[key]}`; + }); + + if (cookie.secure) { + fields.push('secure'); + } + + const propStr = fields.join(';'); + + const newCookie = `${name}=${value}; ${propStr}`; + document.cookie=newCookie; + } + + /** + * If present in the browser, returns the value for the cookie. If not present, returns undefined. + * @param {string} name + */ + function getCookie(name) { + const allCookies = {}; + document.cookie.split(';').forEach((cookie) => { + const parts = cookie.trim().split('='); + allCookies[parts[0]] = parts[1]; + }); + return allCookies[name] || null; + } + + /** + * Removes a cookie from the browser. Meant for removing auth token and narrative session + * cookies on logout. + * @param {string} name + * @param {string} path + * @param {string || undefined} domain + */ + function removeCookie(name, path, domain) { + const cookieToRemove = { + name, + value: '', + path, + expires: 0 + }; + if (domain) { + cookieToRemove.domain = domain; + } + setCookie(cookieToRemove); + } /** * Does a GET request to get the profile of the currently logged in user. @@ -46,30 +160,57 @@ define([ * Returns null if not logged in. */ function getAuthToken() { - if (!$.cookie(cookieName)) { - return null; - } - return $.cookie(cookieName); + return getCookie(cookieConfig.auth.name); } /* Sets the given auth token into the browser's cookie. * Does nothing if the token is null. */ function setAuthToken(token) { - if (token) { - $.cookie(cookieName, token, {path: '/', domain: 'kbase.us', expires: 60}); - $.cookie(cookieName, token, {path: '/', expires: 60}); + const deployEnv = Config.get('environment'); + + function setToken(config) { + // Honor cookie host whitelist if present. + if (config.enableIn) { + if (config.enableIn.indexOf(deployEnv) === -1) { + return; + } + } + const cookieField = { + name: config.name, + value: token + }; + if (config.domain) { + cookieField.domain = config.domain; + } + setCookie(cookieField); } + + setToken(cookieConfig.auth); + setToken(cookieConfig.backup); } - /* Deletes the auth token cookie */ + /* Deletes the auth cookies */ function clearAuthToken() { - $.removeCookie(cookieName, {path: '/'}); - $.removeCookie(cookieName, {path: '/', domain: 'kbase.us'}); + const deployEnv = Config.get('environment'); + + function removeToken(config) { + // Honor the cookie host whitelist if present. + if (config.enableIn) { + if (config.enableIn.indexOf(deployEnv) === -1) { + return; + } + } + removeCookie(config.name, '/', config.domain); + } + + Object.keys(cookieConfig).forEach((name) => { + removeToken(cookieConfig[name]); + }); } function revokeAuthToken(token, id) { - var operation = '/tokens/revoke/' + id; + const operation = '/tokens/revoke/' + id; return makeAuthCall(token, { operation: operation, method: 'DELETE' @@ -82,8 +223,8 @@ define([ if (!token) { token = getAuthToken(); } - let encodedUsers = users.map(u => encodeURIComponent(u)); - var operation = '/users/?list=' + encodedUsers.join(','); + const encodedUsers = users.map((u) => encodeURIComponent(u)); + const operation = '/users/?list=' + encodedUsers.join(','); return makeAuthCall(token, { operation: operation, method: 'GET' @@ -94,7 +235,7 @@ define([ if (!token) { token = getAuthToken(); } - var operation = '/users/search/' + query; + let operation = '/users/search/' + query; if (options) { operation += '/?fields=' + options.join(','); } @@ -129,18 +270,19 @@ define([ * parameters as expected. */ function makeAuthCall(token, callParams) { - var version = callParams.version || 'V2', - callString = [ - url, - '/api/', - version, - callParams.operation - ].join(''); + const version = callParams.version || 'V2'; + const callString = [ + url, + '/api/', + version, + callParams.operation + ].join(''); return Promise.resolve($.ajax({ url: callString, method: callParams.method, dataType: 'json', + crossDomain: true, headers: { 'Authorization': token, 'Content-Type': 'application/json' @@ -165,36 +307,37 @@ define([ retries = 3; } return getTokenInfo(token) - .then(function(info) { - if (info.expires && info.expires > new Date().getTime()) { - return true; - } - return false; - }) - .catch(function(error) { - if (error.status === 401 || error.status === 403 || retries < 0) { + .then(function (info) { + if (info.expires && info.expires > new Date().getTime()) { + return true; + } return false; - } - else { - throw error; - } - }); + }) + .catch(function (error) { + if (error.status === 401 || error.status === 403 || retries < 0) { + return false; + } + else { + throw error; + } + }); } return { - putCurrentProfile: putCurrentProfile, - getCurrentProfile: getCurrentProfile, + putCurrentProfile, + getCurrentProfile, getUserProfile: getCurrentProfile, - getAuthToken: getAuthToken, - setAuthToken: setAuthToken, - clearAuthToken: clearAuthToken, - revokeAuthToken: revokeAuthToken, - getTokenInfo: getTokenInfo, - getUserNames: getUserNames, - searchUserNames: searchUserNames, - validateToken: validateToken + getAuthToken, + setAuthToken, + clearAuthToken, + revokeAuthToken, + getTokenInfo, + getUserNames, + searchUserNames, + validateToken, + setCookie, + getCookie }; - } return { diff --git a/kbase-extension/static/kbase/js/common/jobs.js b/kbase-extension/static/kbase/js/common/jobs.js index a8db5ad5f8..319c8ccce2 100644 --- a/kbase-extension/static/kbase/js/common/jobs.js +++ b/kbase-extension/static/kbase/js/common/jobs.js @@ -28,7 +28,7 @@ define([ /* * Strip out console commands from text captured from console: * http://search.cpan.org/~jlmorel/Win32-Console-ANSI-1.10/lib/Win32/Console/ANSI.pm - * + * */ function consoleToText(consoleText) { return consoleText.replace(/\[([\s\S]*?)m/g, ''); @@ -95,7 +95,7 @@ define([ } /* - * For a given job, returns the log lines after "skip" lines, as an Promise + * For a given job, returns the log lines after "skip" lines, as an Promise * which will deliver an array of strings. */ function getLogData(jobId, skip) { @@ -112,9 +112,28 @@ define([ throw new Error("Method is not supported anymore"); } + /** + * A jobState is deemed valid if + * 1. It's an object (not an array or atomic type) + * 2. It has a created key + * 3. It has a job_id key + * 4. There's others that are necessary, but the top two are sufficient to judge if it's valid + * enough and up to date. This function should be updated as necessary. + * + * This is intended to be used to make sure that jobStates are of the latest version of the + * execution engine. + * @param {object} jobState + */ + function isValidJobState(jobState) { + if (typeof jobState === 'object' && jobState !== null) { + return jobState.hasOwnProperty('created') && jobState.hasOwnProperty('job_id'); + } + return false; + } return { getLogData: getLogData, - deleteJob: deleteJob + deleteJob: deleteJob, + isValidJobState: isValidJobState }; -}); \ No newline at end of file +}); diff --git a/kbase-extension/static/kbase/js/common/props.js b/kbase-extension/static/kbase/js/common/props.js index cabaa129f8..55aae8e636 100644 --- a/kbase-extension/static/kbase/js/common/props.js +++ b/kbase-extension/static/kbase/js/common/props.js @@ -63,7 +63,7 @@ define([], function() { temp[propKey] = []; } temp = temp[propKey]; - // Finally set the property. + // Finally set the property. if (typeof temp === 'object' && temp.push) { temp.push(value); @@ -130,7 +130,7 @@ define([], function() { timer, api; /* - * In enabled by setting an update handler via the onUpdate factory + * In enabled by setting an update handler via the onUpdate factory * configuration property, this function should be run whenever the * property is updated. It will then run the update handler callback. * This is a way to enable essentially synchronization of the props @@ -271,37 +271,6 @@ define([], function() { return temp[propKey]; } - // function pushItem(path, value) { - // if (typeof path === 'string') { - // path = path.split('.'); - // } - // if (path.length === 0) { - // return; - // } - // var propKey = path.pop(), - // key, temp = obj; - // while (path.length > 0) { - // key = path.shift(); - // if (temp[key] === undefined) { - // temp[key] = {}; - // } - // temp = temp[key]; - // } - // ensureHistory(); - // if (temp[propKey] === undefined) { - // temp[propKey] = [value]; - // } else { - // if (temp[propKey]) - // if (isArray(temp[propKey])) { - // temp[propKey].push(value); - // } else { - // throw new Error('Can only push onto an Array'); - // } - // } - // run(); - // return temp[propKey]; - // } - function deleteItem(path) { if (typeof path === 'string') { path = path.split('.'); @@ -354,4 +323,4 @@ define([], function() { pushDataItem: pushDataItem, popDataItem: popDataItem }; -}); \ No newline at end of file +}); diff --git a/kbase-extension/static/kbase/js/kbaseNarrative.js b/kbase-extension/static/kbase/js/kbaseNarrative.js index 0d2f5857a7..db7addf7c0 100644 --- a/kbase-extension/static/kbase/js/kbaseNarrative.js +++ b/kbase-extension/static/kbase/js/kbaseNarrative.js @@ -15,6 +15,7 @@ define([ 'bluebird', 'handlebars', 'narrativeConfig', + 'jobCommChannel', 'kbaseNarrativeSidePanel', 'kbaseNarrativeOutputCell', 'kbaseNarrativeWorkspace', @@ -49,6 +50,7 @@ define([ Promise, Handlebars, Config, + JobCommChannel, KBaseNarrativeSidePanel, KBaseNarrativeOutputCell, KBaseNarrativeWorkspace, @@ -782,24 +784,20 @@ define([ this.sidePanel.render(); }.bind(this)); - $([Jupyter.events]).trigger('loaded.Narrative'); $([Jupyter.events]).on('kernel_ready.Kernel', - function () { - console.log('Kernel Ready! Initializing Job Channel...'); + (e) => { this.loadingWidget.updateProgress('kernel', true); + this.jobCommChannel = new JobCommChannel(); // TODO: This should be an event "kernel-ready", perhaps broadcast // on the default bus channel. - this.sidePanel.$jobsWidget.initCommChannel() - .then(function () { - this.loadingWidget.updateProgress('jobs', true); - }.bind(this)) - .catch(function (err) { + this.jobCommChannel.initCommChannel() + .then(() => this.loadingWidget.updateProgress('jobs', true)) + .catch((err) => { // TODO: put the narrative into a terminal state console.error('ERROR initializing kbase comm channel', err); KBFatal('Narrative.ini', 'KBase communication channel could not be initiated with the back end. TODO'); - // this.loadingWidget.remove(); - }.bind(this)); - }.bind(this) + }); + } ); }.bind(this)); }; diff --git a/kbase-extension/static/kbase/js/util/bootstrapSearch.js b/kbase-extension/static/kbase/js/util/bootstrapSearch.js index 3ffc846ac9..0219305f82 100644 --- a/kbase-extension/static/kbase/js/util/bootstrapSearch.js +++ b/kbase-extension/static/kbase/js/util/bootstrapSearch.js @@ -14,6 +14,7 @@ * inputFunction - gets fired off when a user inputs something. * addonFunction - gets fired off when a user clicks the addon area. default = clear input. * escFunction - gets fired off if escape (key 27) is hit while the input is focused. + * delay - time in ms before firing the input function (default 300) */ define([ @@ -46,6 +47,9 @@ define([ if (!options.filledIcon.startsWith('fa-')) { options.filledIcon = 'fa-' + options.filledIcon; } + if (!options.delay) { + options.delay = 300; + } this.options = options; this.initialize($target); @@ -89,7 +93,7 @@ define([ if (Jupyter && Jupyter.narrative) { Jupyter.narrative.disableKeyboardManager(); } - }).on('input change', function (e) { + }).on('input', function (e, ignoreDelay) { if ($input.val()) { $addonIcon.removeClass(self.options.emptyIcon); $addonIcon.addClass(self.options.filledIcon); @@ -99,7 +103,16 @@ define([ $addonIcon.addClass(self.options.emptyIcon); } if (self.options.inputFunction) { - self.options.inputFunction(e); + if (self.delayTimeout) { + clearTimeout(self.delayTimeout); + } + if (ignoreDelay) { + return self.options.inputFunction(e); + } + self.delayTimeout = setTimeout( + () => self.options.inputFunction(e), + self.options.delay + ); } }).on('keyup', function (e) { if (e.keyCode === 27) { @@ -112,7 +125,7 @@ define([ $addonBtn.click(function(e) { if (!self.options.addonFunction) { $input.val(''); - $input.trigger('input'); + $input.trigger('input', [true]); } else { self.options.addonFunction(e); @@ -130,7 +143,7 @@ define([ } else { retVal = this.$input.val(val); - this.$input.trigger('input'); + this.$input.trigger('input', [true]); } return retVal; }; diff --git a/kbase-extension/static/kbase/js/util/jobLogViewer.js b/kbase-extension/static/kbase/js/util/jobLogViewer.js index 2aa8854ef1..3e259aea10 100644 --- a/kbase-extension/static/kbase/js/util/jobLogViewer.js +++ b/kbase-extension/static/kbase/js/util/jobLogViewer.js @@ -33,13 +33,11 @@ define([ button = t('button'), span = t('span'), p = t('p'), - fsm, - currentSection, smallPanelHeight = '300px', largePanelHeight = '600px', - numLines = 10, - panel, + numLines = 100, panelHeight = smallPanelHeight, + // all the states possible, to be fed into the FSM. appStates = [{ state: { mode: 'new' @@ -340,24 +338,24 @@ define([ /** * The entrypoint to this widget. This creates the job log viewer and initializes it. - * Starting it is left as a lifecycle method for the calling object. + * Starting it is left as a lifecycle method for the caller. * */ function factory() { let runtime = Runtime.make(), - bus = runtime.bus().makeChannelBus({ description: 'Log Viewer Bus' }), container, jobId, - panelId, model, ui, - startingLine = 0, - linesPerPage = null, + linesPerPage = numLines, + fsm, loopFrequency = 5000, looping = false, stopped = false, - listeningForJob = false, - requestLoop = null; + listeningForJob = false, // if true, this means we're listening for job updates + awaitingLog = false, // if true, there's a log request fired that we're awaiting + requestLoop = null, // the timeout object + scrollToEndOnNext = false; // VIEW ACTIONS @@ -365,38 +363,43 @@ define([ if (!looping) { return; } - requestLoop = window.setTimeout(function() { - if (!looping) { - return; - } + requestLoop = window.setTimeout(() => { requestLatestJobLog(); }, loopFrequency); } function stopAutoFetch() { looping = false; + if (ui) { + ui.hideElement('spinner'); + } } + /** + * Starts the autofetch loop. After the first request, this starts a timeout that calls it again. + */ function startAutoFetch() { - if (looping) { - return; - } - if (stopped) { + if (looping || stopped) { return; } var state = fsm.getCurrentState().state; if (state.mode === 'active' && state.auto) { looping = true; fsm.newState({ mode: 'active', auto: true }); - runtime.bus().emit('request-latest-job-log', { - jobId: jobId, - options: { - num_lines: linesPerPage - } - }); + // stop the current timer if we have one. + if (requestLoop) { + clearTimeout(requestLoop); + } + requestLatestJobLog(); + // runtime.bus().emit('request-latest-job-log', { + // jobId: jobId, + // }); } } + /** + * Start automatically fetching logs - triggered by hitting the play button. + */ function doPlayLogs() { fsm.updateState({ auto: true @@ -405,21 +408,32 @@ define([ startAutoFetch(); } - function doStopPlayLogs() { + /** + * Stop automatically fetching logs - triggered by hitting the stop button. + */ + function doStopLogs() { fsm.updateState({ auto: false }); stopped = true; stopAutoFetch(); + if (requestLoop) { + clearTimeout(requestLoop); + } } - function requestJobLog(firstLine, numLines, params) { + /** + * Requests numLines (set in the factory method) log lines starting from the given firstLine. + * @param {int} firstLine + */ + function requestJobLog(firstLine) { ui.showElement('spinner'); + awaitingLog = true; runtime.bus().emit('request-job-log', { jobId: jobId, options: { first_line: firstLine, - num_lines: linesPerPage + // num_lines: linesPerPage } }); } @@ -429,14 +443,13 @@ define([ // load numLines at a time // otherwise load entire log let autoState = fsm.getCurrentState().state.auto; - if(autoState){ - linesPerPage = numLines; // set it to 10 - } + scrollToEndOnNext = true; + awaitingLog = true; ui.showElement('spinner'); runtime.bus().emit('request-latest-job-log', { jobId: jobId, options: { - num_lines: linesPerPage + // num_lines: linesPerPage } }); } @@ -445,21 +458,22 @@ define([ * Scroll to the top of the job log */ function doFetchFirstLogChunk() { - doStopPlayLogs(); - panel.scrollTo(0, 0); + doStopLogs(); + requestJobLog(0); + getLogPanel().scrollTo(0, 0); } /** * scroll to the bottom of the job log */ function doFetchLastLogChunk() { - doStopPlayLogs(); - panel.scrollTo(0, panel.lastChild.offsetTop); + doStopLogs(); + requestLatestJobLog(); } function toggleViewerSize() { panelHeight = panelHeight === smallPanelHeight ? largePanelHeight : smallPanelHeight; - getPanelNode().style.height = panelHeight; + getLogPanel().style.height = panelHeight; } // VIEW @@ -503,7 +517,7 @@ define([ title: 'Stop fetching logs', id: events.addEvent({ type: 'click', - handler: doStopPlayLogs + handler: doStopLogs }) }, [ span({ class: 'fa fa-stop' }) @@ -541,11 +555,40 @@ define([ ]); } + /** + * This is a step toward having scrollahead/scrollbehind. It doesn't work right, and we + * have to move on, but I'm leaving this in here for now. + * There's something minor that I'm missing, I think, about how the scrolling gets + * managed. + * @param {ScrollEvent} e + */ + function handlePanelScrolling(e) { + const panel = getLogPanel(); + // if scroll is at the bottom, and there are more lines, + // get the next chunk. + if (panel.scrollTop === (panel.scrollHeight - panel.offsetHeight)) { + const curLast = model.getItem('lastLine'); + if (curLast < model.getItem('totalLines')) { + requestJobLog(curLast); + } + } + // if it's at the top, and we're not at line 0, get + // the previous chunk. + else if (panel.scrollTop === 0) { + const curFirst = model.getItem('firstLine'); + if (curFirst > 0) { + const reqLine = Math.max(0, curFirst - numLines); + if (reqLine < curFirst) { + requestJobLog(reqLine); + } + } + } + } + /** * builds contents of panel-body class - * @param {string} panelId */ - function renderLayout(panelId) { + function renderLayout() { const events = Events.make(), content = div({ dataElement: 'kb-log', style: { marginTop: '10px'}}, [ div({ class: 'kblog-header' }, [ @@ -556,7 +599,7 @@ define([ renderControls(events) // header ]) ]), - div({ dataElement: 'panel', id: panelId, + div({ dataElement: 'log-panel', style: { 'overflow-y': 'scroll', height: panelHeight, @@ -571,24 +614,6 @@ define([ }; } - function sanitize(text) { - var longWord = 80; - var encoded = ui.htmlEncode(text); - - // try to make sane word length not break things. - var words = encoded.split(/ /); - - var fixed = words.map(function(word) { - if (word.length < longWord) { - return word; - } - return word.replace(/\//, '/') - .replace(/\./, '.'); - }); - - return fixed.join(' '); - } - /** * build and return div that displays * individual job log line @@ -616,7 +641,7 @@ define([ // text const textDiv = document.createElement('div'); textDiv.setAttribute('class', 'kblog-text'); - const lineText = document.createTextNode(line.text) + const lineText = document.createTextNode(line.text); textDiv.appendChild(lineText); // append line number and text wrapperDiv.appendChild(numDiv); @@ -627,15 +652,8 @@ define([ return kblogLine; } - /** - * Append div that displays job log lines - * to the panel - * @param {array} lines - */ - function makeLogChunkDiv(lines) { - for (let i=0; i panel.appendChild(buildLine(line))); - makeLogChunkDiv(viewLines); - if (fsm.getCurrentState().state.auto) { + // if we're autoscrolling, scroll to the bottom + if (fsm.getCurrentState().state.auto || scrollToEndOnNext) { panel.scrollTo(0, panel.lastChild.offsetTop); + scrollToEndOnNext = false; } } else { - ui.setContent('panel', 'Sorry, no log yet...'); + ui.setContent('log-panel', 'No log entries to show.'); } } function handleJobStatusUpdate(message) { // if the job is finished, we don't want to reflect // this in the ui, and disable play/stop controls. - var jobStatus = message.jobState.job_state, + var jobStatus = message.jobState.status, mode = fsm.getCurrentState().state.mode, newState; switch (mode) { case 'new': switch (jobStatus) { + case 'created': + case 'estimating': case 'queued': startJobUpdates(); newState = { @@ -687,7 +700,7 @@ define([ auto: true }; break; - case 'in-progress': + case 'running': startJobUpdates(); startAutoFetch(); newState = { @@ -696,22 +709,21 @@ define([ }; break; case 'completed': - requestLatestJobLog(); + requestJobLog(0); stopJobUpdates(); newState = { mode: 'complete' }; break; case 'error': - case 'suspend': - requestLatestJobLog(); + requestJobLog(0); stopJobUpdates(); newState = { mode: 'error' }; break; - case 'canceled': - requestLatestJobLog(); + case 'terminated': + requestJobLog(0); stopJobUpdates(); newState = { mode: 'canceled' @@ -725,10 +737,12 @@ define([ break; case 'queued': switch (jobStatus) { + case 'created': + case 'estimating': case 'queued': // no change break; - case 'in-progress': + case 'running': newState = { mode: 'active', auto: true @@ -741,12 +755,11 @@ define([ }; break; case 'error': - case 'suspend': newState = { mode: 'error' }; break; - case 'canceled': + case 'terminated': newState = { mode: 'canceled' }; @@ -761,7 +774,7 @@ define([ case 'queued': // this should not occur! break; - case 'in-progress': + case 'running': startAutoFetch(); break; case 'completed': @@ -770,12 +783,11 @@ define([ }; break; case 'error': - case 'suspend': newState = { mode: 'error' }; break; - case 'canceled': + case 'terminated': newState = { mode: 'canceled' }; @@ -795,7 +807,7 @@ define([ } case 'canceled': switch (jobStatus) { - case 'canceled': + case 'terminated': return; default: console.error('Unexpected log status ' + jobStatus + ' for "canceled" state'); @@ -836,32 +848,43 @@ define([ type: 'job-logs' }, handle: function(message) { + if (!awaitingLog) { + return; + } ui.hideElement('spinner'); + /* message has structure: + * { + * jobId: string, + * latest: bool, + * logs: { + * first: int (first line of the log batch), 0-indexed + * job_id: string, + * latest: bool, + * max_lines: int (total logs available - if job is done, so is this), + * lines: [{ + * is_error: 0 or 1, + * line: string, + * linepos: int, position in log. helpful! + * ts: timestamp + * }] + * } + * } + */ + awaitingLog = false; - if (message.logs.lines.length === 0) { - // TODO: add an alert area and show a dismissable alert. - if (!looping) { - // alert('No log entries returned'); - console.warn('No log entries returned', message); - } - } else { - var lines = model.getItem('lines'); - model.setItem('lines', message.logs.lines); - model.setItem('currentLine', message.logs.first); - model.setItem('latest', true); - model.setItem('fetchedAt', new Date().toUTCString()); - // Detect end of log. - var lastLine = model.getItem('lastLine'), - batchLastLine = message.logs.first + message.logs.lines.length; - if (!lastLine) { - lastLine = batchLastLine; - } else { - if (batchLastLine > lastLine) { - lastLine = batchLastLine; - } - } - model.setItem('lastLine', lastLine); - + if (message.logs.lines.length !== 0) { + const viewLines = message.logs.lines.map(function(line, index) { + return { + text: line.line, + isError: (line.is_error === 1 ? true : false), + lineNumber: line.linepos + }; + }); + model.setItem('lines', viewLines); + model.setItem('firstLine', message.logs.first + 1); + model.setItem('lastLine', message.logs.first + viewLines.length); + model.setItem('totalLines', message.logs.max_lines); + render(); } if (looping) { scheduleNextRequest(); @@ -885,7 +908,7 @@ define([ }, handle: function() { stopAutoFetch(); - console.warn('No job log :( -- it has been deleted'); + render(); } }); externalEventListeners.push(ev); @@ -953,7 +976,7 @@ define([ text: 'Job is queued, logs will be available when the job is running.' } const line = buildLine(noLogYet); - ui.setContent('kb-log.panel', line); + getLogPanel().appendChild(line); } function doExitQueued(message) { @@ -1005,9 +1028,6 @@ define([ } function startJobUpdates() { - if (listeningForJob) { - return; - } runtime.bus().emit('request-job-update', { jobId: jobId }); @@ -1020,10 +1040,6 @@ define([ } } - function getPanelNode() { - return document.getElementById(panelId); - } - /** * The main lifecycle event, called when its container node exists, and we want to start * running this widget. @@ -1046,11 +1062,9 @@ define([ container = hostNode.appendChild(document.createElement('div')); ui = UI.make({ node: container }); - panelId = html.genId(); - var layout = renderLayout(panelId); + var layout = renderLayout(); container.innerHTML = layout.content; layout.events.attachEvents(container); - panel = getPanelNode(); initializeFSM(); renderFSM(); @@ -1069,9 +1083,6 @@ define([ if (requestLoop) { clearTimeout(requestLoop); } - if (bus) { - bus.stop(); - } if (fsm) { fsm.stop(); } @@ -1086,17 +1097,25 @@ define([ } // MAIN + /* The data model for this widget contains all lines currently being shown, along with the indices for + * the first and last lines known. + * It also tracks the total lines currently available for the app, if returned. + * Lines is a list of small objects. Each object + * lines - list, each is a small object with keys (these are all post-processed after fetching): + * line - string, the line text + * isError - boolean, true if that line denotes an error + * ts - int timestamp + * lineNumber - int, what line this is + * firstLine - int, the first line we're tracking (inclusive) + * lastLine - int, the last line we're tracking (inclusive) + * totalLines - int, the total number of lines available from the server as of the last message. + */ model = Props.make({ data: { - cache: [], lines: [], - currentLine: null, + firstLine: null, lastLine: null, - linesPerPage: linesPerPage, - fetchedAt: null - }, - onUpdate: function() { - render(); + totalLines: null } }); @@ -1105,7 +1124,6 @@ define([ return Object.freeze({ start: start, stop: stop, - bus: bus, detach: detach }); } diff --git a/kbase-extension/static/kbase/js/widgets/function_output/kbaseSampleSet.js b/kbase-extension/static/kbase/js/widgets/function_output/kbaseSampleSet.js new file mode 100644 index 0000000000..769f8c6595 --- /dev/null +++ b/kbase-extension/static/kbase/js/widgets/function_output/kbaseSampleSet.js @@ -0,0 +1,231 @@ +/* +Sebastian Le Bras - April 2020 +*/ +define ([ + 'kbwidget', + 'bootstrap', + 'jquery', + 'kbase-client-api', + 'widgets/dynamicTable', + 'kbaseAuthenticatedWidget', + 'kbaseTabs', + 'kbase-generic-client-api', + 'narrativeConfig', + 'bluebird' +], function( + KBWidget, + bootstrap, + $, + kbase_client_api, + DynamicTable, + kbaseAuthenticatedWidget, + kbaseTabs, + GenericClient, + Config, + Promise +) { + 'use strict'; + + return KBWidget({ + name: 'kbaseSampleSetView', + parent: kbaseAuthenticatedWidget, + version: '1.0.0', + options: { + pageLimit: 10, + default_blank_value: "" + }, + + init: function (options) { + this._super(options); + + // var self = this; + this.obj_ref = this.options.upas.id; + this.link_ref = this.obj_ref; + + if(options._obj_info) { + this.ss_info = options._obj_info; + this.obj_ref = this.ss_info['ws_id'] + '/' + this.ss_info['id'] + '/' + this.ss_info['version']; + this.link_ref = this.ss_info['ws_id'] + '/' + this.ss_info['name'] + '/' + this.ss_info['version']; + } + + this.client = new GenericClient(Config.url('service_wizard'), {token: this.authToken()}); + this.ws = new Workspace(Config.url('workspace'),{'token': this.authToken()}); + + this.$elem.append($('
').attr('align', 'center').append($(''))); + + // 1) get stats, and show the panel + var basicInfoCalls = []; + basicInfoCalls.push( + Promise.resolve(this.ws.get_objects2({objects: [{'ref': this.obj_ref}]})) + .then((obj) => { + this.ss_obj_data = obj['data'][0]['data'] + this.ss_obj_info = obj['data'][0]['info'] + this.link_ref = this.ss_obj_info[6] + '/' + this.ss_obj_info[0] + '/' + this.ss_obj_info[4]; + })); + + Promise.all(basicInfoCalls) + .then(() => { + this.renderBasicTable(); + }) + .catch((err) => { + console.error('an error occurred! ' + err); + this.$elem.empty(); + this.$elem.append('An unexpected error occured: \nPlease contact KBase here'); + }); + + return this; + }, + + renderBasicTable: function() { + var self = this; + var $container = this.$elem; + $container.empty(); + + var $tabPane = $('
'); + $container.append($tabPane); + + + // Build the overview table + var $overviewTable = $(''); + + function get_table_row(key, value) { + return $('').append($('", html) + self.assertIn("", html) + self.assertIn("", html) + self.assertIn("", html) + self.assertIn("", html) + self.assertIn("", html) + self.assertIn("", html) @mock.patch('biokbase.narrative.jobs.jobmanager.clients.get', get_mock_client) def test_cancel_job_good(self): @@ -89,70 +90,77 @@ def test_cancel_job_good(self): job_id = new_job.job_id self.jm.register_new_job(new_job) self.jm.cancel_job(job_id) - self.jm.delete_job(job_id) def test_cancel_job_bad(self): with self.assertRaises(ValueError): self.jm.cancel_job(None) @mock.patch('biokbase.narrative.jobs.jobmanager.clients.get', get_mock_client) - def test_job_status_control(self): - self.jm._handle_comm_message(create_jm_message("start_update_loop")) - self.jm._handle_comm_message(create_jm_message("stop_update_loop")) - - @mock.patch('biokbase.narrative.jobs.jobmanager.clients.get', get_mock_client) - def test_job_status_fetching(self): - self.jm._handle_comm_message(create_jm_message("all_status")) - msg = self.jm._comm.last_message - job_data = msg.get('data', {}).get('content', {}) - job_ids = list(job_data.keys()) - # assert that each job info that's flagged for lookup gets returned - jobs_to_lookup = [j for j in self.jm._running_jobs.keys()] - self.assertCountEqual(job_ids, jobs_to_lookup) - for job_id in job_ids: - self.assertTrue(self.validate_status_message(job_data[job_id])) - self.jm._comm.clear_message_cache() - - @mock.patch('biokbase.narrative.jobs.jobmanager.clients.get', get_mock_client) - def test_single_job_status_fetch(self): - new_job = phony_job() - self.jm.register_new_job(new_job) - self.jm._handle_comm_message(create_jm_message("job_status", new_job.job_id)) - msg = self.jm._comm.last_message - self.assertEqual(msg['data']['msg_type'], "job_status") - self.assertTrue(self.validate_status_message(msg['data']['content'])) - self.jm.delete_job(new_job.job_id) - self.jm._comm.clear_message_cache() + def test_lookup_all_job_states(self): + states = self.jm.lookup_all_job_states() + self.assertEqual(len(states), 2) + + states = self.jm.lookup_all_job_states(ignore_refresh_flag=True) + self.assertEqual(len(states), 3) + + # @mock.patch('biokbase.narrative.jobs.jobmanager.clients.get', get_mock_client) + # def test_job_status_fetching(self): + # self.jm._handle_comm_message(create_jm_message("all_status")) + # msg = self.jm._comm.last_message + # job_data = msg.get('data', {}).get('content', {}) + # job_ids = list(job_data.keys()) + # # assert that each job info that's flagged for lookup gets returned + # jobs_to_lookup = [j for j in self.jm._running_jobs.keys()] + # self.assertCountEqual(job_ids, jobs_to_lookup) + # for job_id in job_ids: + # self.assertTrue(self.validate_status_message(job_data[job_id])) + # self.jm._comm.clear_message_cache() + + # @mock.patch('biokbase.narrative.jobs.jobmanager.clients.get', get_mock_client) + # def test_single_job_status_fetch(self): + # new_job = phony_job() + # self.jm.register_new_job(new_job) + # self.jm._handle_comm_message(create_jm_message("job_status", new_job.job_id)) + # msg = self.jm._comm.last_message + # self.assertEqual(msg['data']['msg_type'], "job_status") + # # self.assertTrue(self.validate_status_message(msg['data']['content'])) + # self.jm._comm.clear_message_cache() # Should "fail" based on sent message. - def test_job_message_bad_id(self): - self.jm._handle_comm_message(create_jm_message("foo", job_id="not_a_real_job")) - msg = self.jm._comm.last_message - self.assertEqual(msg['data']['msg_type'], 'job_does_not_exist') + # def test_job_message_bad_id(self): + # self.jm._handle_comm_message(create_jm_message("foo", job_id="not_a_real_job")) + # msg = self.jm._comm.last_message + # self.assertEqual(msg['data']['msg_type'], 'job_does_not_exist') def test_cancel_job_lookup(self): pass - @mock.patch('biokbase.narrative.jobs.jobmanager.clients.get', get_mock_client) - def test_stop_single_job_lookup(self): - # Set up and make sure the job gets returned correctly. - new_job = phony_job() - phony_id = new_job.job_id - self.jm.register_new_job(new_job) - self.jm._handle_comm_message(create_jm_message("start_job_update", job_id=phony_id)) - self.jm._handle_comm_message(create_jm_message("stop_update_loop")) - - self.jm._lookup_all_job_status() - msg = self.jm._comm.last_message - self.assertTrue(phony_id in msg['data']['content']) - self.assertEqual(msg['data']['content'][phony_id].get('listener_count', 0), 1) - self.jm._comm.clear_message_cache() - self.jm._handle_comm_message(create_jm_message("stop_job_update", job_id=phony_id)) - self.jm._lookup_all_job_status() - msg = self.jm._comm.last_message - self.assertTrue(self.jm._running_jobs[phony_id]['refresh'] == 0) - self.assertIsNone(msg) - + # @mock.patch('biokbase.narrative.jobs.jobmanager.clients.get', get_mock_client) + # def test_stop_single_job_lookup(self): + # # Set up and make sure the job gets returned correctly. + # new_job = phony_job() + # phony_id = new_job.job_id + # self.jm.register_new_job(new_job) + # self.jm._handle_comm_message(create_jm_message("start_job_update", job_id=phony_id)) + # self.jm._handle_comm_message(create_jm_message("stop_update_loop")) + + # self.jm._lookup_all_job_status() + # msg = self.jm._comm.last_message + # self.assertTrue(phony_id in msg['data']['content']) + # self.assertEqual(msg['data']['content'][phony_id].get('listener_count', 0), 1) + # self.jm._comm.clear_message_cache() + # self.jm._handle_comm_message(create_jm_message("stop_job_update", job_id=phony_id)) + # self.jm._lookup_all_job_status() + # msg = self.jm._comm.last_message + # self.assertTrue(self.jm._running_jobs[phony_id]['refresh'] == 0) + # self.assertIsNone(msg) + + @mock.patch('biokbase.narrative.jobs.jobmanager.clients.get', get_failing_mock_client) + def test_initialize_jobs_ee2_fail(self): + # init jobs should fail. specifically, ee2.check_workspace_jobs should error. + with self.assertRaises(NarrativeException) as e: + self.jm.initialize_jobs() + self.assertIn('Job lookup failed', str(e.exception)) if __name__ == "__main__": unittest.main() diff --git a/src/biokbase/narrative/tests/test_narrativeexporting.py b/src/biokbase/narrative/tests/test_narrativeexporting.py deleted file mode 100644 index d78bb1d6ce..0000000000 --- a/src/biokbase/narrative/tests/test_narrativeexporting.py +++ /dev/null @@ -1,70 +0,0 @@ -from biokbase.narrative.common.exceptions import WorkspaceError -from biokbase.workspace.baseclient import ServerError -from biokbase.narrative.exporter.exporter import NarrativeExporter -import unittest -import os -import mock -from .util import TestConfig - -""" -Some tests for narrative exporting. -""" -__author__ = "Bill Riehl " - -output_file = "test.html" -config = TestConfig() - - -def mock_read_narrative(style): - """ - Mocks the NarrativeIO.read_narrative() function. - Style should be one of "good", "bad", or "private". - - A "good" narrative will just return the valid read_narrative() - results by loading and returning the given file. (will raise a - ValueError if file is None). - - A "bad" narrative will raise a ValueError, and a "private" - style will raise a NarrativeIO.PermissionsError. - """ - if style == test_narrative_ref: - return config.load_json_file(config.get('narrative_refs', 'narr_file')) - elif style == bad_narrative_ref: - raise ValueError('Bad Narrative!') - elif style == private_narrative_ref: - raise WorkspaceError(ServerError("Error", -32500, "Private workspace!"), private_narrative_ref) - -test_narrative_ref = config.get('narrative_refs', 'public') -private_narrative_ref = config.get('narrative_refs', 'private') -bad_narrative_ref = config.get('narrative_refs', 'bad') - - -class NarrativeExportTesting(unittest.TestCase): - @classmethod - @mock.patch('biokbase.narrative.exporter.exporter.NarrativeIO') - def setUpClass(self, mock_io): - mock_io.return_value.test_connection.return_value = "" - mock_io.return_value.read_narrative = mock_read_narrative - self.exporter = NarrativeExporter() - - @classmethod - def tearDownClass(self): - # delete generated file - if os.path.isfile(output_file): - os.remove(output_file) - - def test_export_good(self): - self.exporter.export_narrative(test_narrative_ref, output_file) - self.assertTrue(os.path.isfile(output_file)) - - def test_export_bad(self): - with self.assertRaises(ValueError): - self.exporter.export_narrative(bad_narrative_ref, output_file) - - def test_export_private(self): - with self.assertRaises(WorkspaceError): - self.exporter.export_narrative(private_narrative_ref, output_file) - - -if __name__ == "__main__": - unittest.main() diff --git a/src/biokbase/narrative/tests/test_widgetmanager.py b/src/biokbase/narrative/tests/test_widgetmanager.py index 714b1c0903..4e2fa62b2d 100644 --- a/src/biokbase/narrative/tests/test_widgetmanager.py +++ b/src/biokbase/narrative/tests/test_widgetmanager.py @@ -190,6 +190,36 @@ def test_infer_upas_simple_widget(self): self.assertIsInstance(upas, dict) self.assertFalse(upas) + @mock.patch('biokbase.narrative.widgetmanager.clients.get', get_mock_client) + def test_infer_upas_nulls(self): + """ + Test infer_upas when None is passed to it as an object name. Fields with None + as input should not map to an UPA. + """ + test_result_upa = "18836/5/1" + upas = self.wm.infer_upas("testCrazyExample", { + "obj_id1": None, + "obj_id2": None, + "obj_name1": "foo", + "obj_name2": "bar/baz", + "obj_names": ["a", "b", "c"], + "obj_ref1": "1/2/3", + "obj_ref2": "foo/bar", + "obj_refs": ["7/8/9", "0/1/2"], + "ws_name": "some_ws", + "extra_param": "extra_value", + "other_extra_param": 0 + }) + self.assertIsInstance(upas, dict) + self.assertNotIn("obj_id1", upas) + self.assertNotIn("obj_id2", upas) + self.assertEqual(upas['obj_name1'], test_result_upa) + self.assertEqual(upas['obj_name2'], test_result_upa) + self.assertEqual(upas['obj_names'], [test_result_upa]*3) + self.assertEqual(upas['obj_ref1'], "1/2/3") + self.assertEqual(upas['obj_ref2'], test_result_upa) + self.assertEqual(upas['obj_refs'], [test_result_upa]*2) + @mock.patch('biokbase.narrative.widgetmanager.clients.get', get_mock_client) def test_missing_env_path(self): backup_dir = os.environ["NARRATIVE_DIR"] diff --git a/src/biokbase/narrative/tests/util.py b/src/biokbase/narrative/tests/util.py index 49591a2a2e..2dce3f9289 100644 --- a/src/biokbase/narrative/tests/util.py +++ b/src/biokbase/narrative/tests/util.py @@ -296,5 +296,43 @@ def find_free_port() -> int: return s.getsockname()[1] +def validate_job_state(state: dict) -> None: + """ + Validates the structure and entries in a job state as returned by the JobManager. + If any keys are missing, or extra keys exist, or values are weird, then this + raises an AssertionError. + """ + # list of tuples - first = key name, second = value type + # details for other cases comes later. This is just the expected basic set of + # keys for EVERY job, once it's been created in EE2. + expected_state_keys = [ + ("job_id", str), + ("status", str), + ("created", int), + ("updated", int), + ("run_id", str), + ("cell_id", str) + ] + optional_state_keys = [ + ("queued", int), + ("finished", int), + ("terminated_code", int), + ("parent_job_id", str), + ("errormsg", str), + ("error", dict), + ("error_code", int) + ] + assert "state" in state, "state key missing" + assert isinstance(state["state"], dict), "state is not a dict" + assert "owner" in state, "owner key missing" + assert isinstance(state["owner"], str), "owner is not a string" + for k in expected_state_keys: + assert k[0] in state["state"], f"{k[0]} key is missing from state" + assert isinstance(state["state"][k[0]], k[1]), f"{k[0]} is not a {k[1]}" + for k in optional_state_keys: + if k in state["state"]: + assert isinstance(state["state"][k[0]], (k[1], None)), f"Optional key {k[0]} is present and not {k[1]} or None" + + if __name__ == '__main__': unittest.main() diff --git a/src/biokbase/narrative/widgetmanager.py b/src/biokbase/narrative/widgetmanager.py index f77e2622e5..9b6caec8ef 100644 --- a/src/biokbase/narrative/widgetmanager.py +++ b/src/biokbase/narrative/widgetmanager.py @@ -127,7 +127,7 @@ def load_widget_info(self, tag="release", verbose=False): widget_name = method['widgets']['output'] if widget_name == 'null': if verbose: - print("Ignoring a widget named 'null' in {} - {}".format(tag, method['info']['id'])) + print(f"Ignoring a widget named 'null' in {tag} - {method['info']['id']}") continue out_mapping = method['behavior'].get('kb_service_output_mapping', method['behavior'].get('output_mapping', None)) if out_mapping is not None: @@ -321,7 +321,7 @@ def show_output_widget(self, widget_name, params, upas=None, tag="release", titl input_data.update(params) if cell_id is not None: - cell_id = "\"{}\"".format(cell_id) + cell_id = f"\"{cell_id}\"" if upas is None: # infer what it is based on mapping and inputs @@ -396,7 +396,7 @@ def infer_upas(self, widget_name, params): obj_ref_list = list() # list of tuples, but second is a list of upas ws = None for param in params.keys(): - if param in param_to_context: + if param in param_to_context and params.get(param) is not None: context = param_to_context[param] if context == "ws_id" or context == "ws_name": ws = params[param] @@ -431,7 +431,7 @@ def infer_upas(self, widget_name, params): info_params.append({"ref": ref}) lookup_params.append(param) else: - raise ValueError('Parameter {} has value {} which was expected to refer to an object'.format(param, ref)) + raise ValueError(f'Parameter {param} has value {ref} which was expected to refer to an object') # params for get_object_info3 for (param, name) in obj_names: @@ -443,7 +443,7 @@ def infer_upas(self, widget_name, params): info_params.append({"ref": name}) lookup_params.append(param) else: - info_params.append({"ref": "{}/{}".format(ws, name)}) + info_params.append({"ref": f"{ws}/{name}"}) lookup_params.append(param) if (len(lookup_params)): @@ -461,7 +461,7 @@ def infer_upas(self, widget_name, params): # actually uniform. for ref in ref_list: if not is_ref(str(ref)): - raise ValueError('Parameter {} has value {} which contains an item that is not a valid object reference'.format(param, ref_list)) + raise ValueError(f'Parameter {param} has value {ref_list} which contains an item that is not a valid object reference') lookup_params.append(param) info_params.append([{'ref': ref} for ref in ref_list]) @@ -471,7 +471,7 @@ def infer_upas(self, widget_name, params): if is_ref(str(name)): info_param.append({'ref': name}) else: - info_param.append({'ref': "{}/{}".format(ws, name)}) + info_param.append({'ref': f"{ws}/{name}"}) info_params.append(info_param) lookup_params.append(param) @@ -518,7 +518,7 @@ def show_advanced_viewer_widget(self, widget_name, params, output_state, tag="re input_data.update(params) if cell_id is not None: - cell_id = "\"{}\"".format(cell_id) + cell_id = f"\"{cell_id}\"" input_template = """ element.html("
"); @@ -583,15 +583,15 @@ def show_data_widget(self, upa, title=None, cell_id=None, tag="release"): if type_spec is None: widget_data = { "error": { - "msg": "Unable to find viewer specification for objects of type {}.".format(bare_type), + "msg": f"Unable to find viewer specification for objects of type {bare_type}.", "method_name": "WidgetManager.show_data_widget", - "traceback": "Can't find type spec info for type {}".format(bare_type) + "traceback": f"Can't find type spec info for type {bare_type}" } } upas['upas'] = [upa] # doompety-doo else: if not type_spec.get('view_method_ids'): - return "No viewer found for objects of type {}".format(bare_type) + return f"No viewer found for objects of type {bare_type}" app_id = type_spec['view_method_ids'][0] app_spec = None try: @@ -599,7 +599,7 @@ def show_data_widget(self, upa, title=None, cell_id=None, tag="release"): except Exception as e: widget_data = { "error": { - "msg": "Unable to find specification for viewer app {}".format(app_id), + "msg": f"Unable to find specification for viewer app {app_id}", "method_name": "WidgetManager.show_data_widget", "traceback": e.message } diff --git a/src/config.json b/src/config.json index 0652ea8794..564098d748 100644 --- a/src/config.json +++ b/src/config.json @@ -1,6 +1,6 @@ { - "config": "dev", "appdev": { + "KBaseSearchEngine": "https://appdev.kbase.us/services/searchapi2/legacy", "auth": "https://appdev.kbase.us/services/auth", "awe": "https://appdev.kbase.us/services/awe-api", "catalog": "https://appdev.kbase.us/services/catalog", @@ -8,6 +8,7 @@ "compound_img_url": "http://minedatabase.mcs.anl.gov/compound_images/ModelSEED/", "data_import_export": "https://appdev.kbase.us/services/data_import_export", "data_panel_sources": "/data_source_config.json", + "execution_engine2": "https://appdev.kbase.us/services/ee2", "fba": "https://appdev.kbase.us/services/KBaseFBAModeling/", "ftp_api_root": "/data/bulk", "ftp_api_url": "https://appdev.kbase.us/services/kb-ftp-api/v0", @@ -15,13 +16,11 @@ "genomeCmp": "https://appdev.kbase.us/services/genome_comparison/jsonrpc", "groups": "https://appdev.kbase.us/services/groups", "job_service": "https://appdev.kbase.us/services/njs_wrapper", - "KBaseSearchEngine": "https://appdev.kbase.us/services/searchapi2/legacy", "landing_pages": "/#dataview/", - "provenance_view": "/#objgraphview", - "log_proxy_host": "172.17.42.1", - "log_proxy_port": 32001, "log_host": "https://elasticsearch2.chicago.kbase.us", "log_port": 9000, + "log_proxy_host": "172.17.42.1", + "log_proxy_port": 32001, "login": "https://appdev.kbase.us/services/authorization/Sessions/Login", "narrative_job_proxy": "https://appdev.kbase.us/services/narrativejobproxy/", "narrative_method_store": "https://appdev.kbase.us/services/narrative_method_store/rpc", @@ -29,12 +28,13 @@ "narrative_method_store_types": "https://next.kbase.us/services/narrative_method_store/rpc", "profile_page": "/#people/", "protein_info": "", + "provenance_view": "/#objgraphview", "reset_password": "https://gologin.kbase.us/ResetPassword", "search": "https://appdev.kbase.us/services/search/getResults", "service_wizard": "https://appdev.kbase.us/services/service_wizard", "shock": "https://appdev.kbase.us/services/shock-api", "staging_api_url": "https://appdev.kbase.us/services/staging_service", - "static_narrative_root": "https://appdev.kbase.us/n", + "static_narrative_root": "https://appdev.kbase.us/sn", "status_page": "http://kbase.us/internal/status/", "submit_jira_ticket": "https://atlassian.kbase.us/secure/CreateIssueDetails!init.jspa?pid=10200&issuetype=1&description=Narrative%20version", "transform": "https://appdev.kbase.us/services/transform", @@ -46,11 +46,13 @@ "user_profile": "https://appdev.kbase.us/services/user_profile/rpc", "version_check": "/narrative_version", "workspace": "https://appdev.kbase.us/services/ws", - "ws_browser": "https://appdev.kbase.us/#ws" + "ws_browser": "https://appdev.kbase.us/#ws", + "google_analytics_id": "UA-74533556-1" }, "auth_cookie": "kbase_session", "auth_sleep_recheck_ms": 60000, "ci": { + "KBaseSearchEngine": "https://ci.kbase.us/services/searchapi2/legacy", "auth": "https://ci.kbase.us/services/auth", "awe": "https://ci.kbase.us/services/awe-api", "catalog": "https://ci.kbase.us/services/catalog", @@ -58,6 +60,7 @@ "compound_img_url": "http://minedatabase.mcs.anl.gov/compound_images/ModelSEED/", "data_import_export": "https://ci.kbase.us/services/data_import_export", "data_panel_sources": "/data_source_config.json", + "execution_engine2": "https://ci.kbase.us/services/ee2", "fba": "https://ci.kbase.us/services/KBaseFBAModeling/", "ftp_api_root": "/data/bulk", "ftp_api_url": "https://ci.kbase.us/services/kb-ftp-api/v0", @@ -66,9 +69,7 @@ "groups": "https://ci.kbase.us/services/groups", "invocation": "", "job_service": "https://ci.kbase.us/services/njs_wrapper", - "KBaseSearchEngine": "https://ci.kbase.us/services/searchapi2/legacy", "landing_pages": "/#dataview/", - "provenance_view": "/#objgraphview", "log_host": "https://elasticsearch2.chicago.kbase.us", "log_port": 9000, "log_proxy_host": "172.17.0.1", @@ -78,11 +79,13 @@ "narrative_method_store_image": "https://ci.kbase.us/services/narrative_method_store/", "profile_page": "/#people/", "protein_info": "", + "provenance_view": "/#objgraphview", "reset_password": "https://gologin.kbase.us/ResetPassword", "search": "https://ci.kbase.us/services/search/getResults", "service_wizard": "https://ci.kbase.us/services/service_wizard", "shock": "https://ci.kbase.us/services/shock-api", "staging_api_url": "https://ci.kbase.us/services/staging_service", + "static_narrative_root": "https://ci.kbase.us/sn", "status_page": "http://kbase.us/internal/status/", "submit_jira_ticket": "https://atlassian.kbase.us/secure/CreateIssueDetails!init.jspa?pid=10200&issuetype=1&description=Narrative%20version", "transform": "https://ci.kbase.us/services/transform", @@ -94,9 +97,13 @@ "user_profile": "https://ci.kbase.us/services/user_profile/rpc", "version_check": "/narrative_version", "workspace": "https://ci.kbase.us/services/ws", - "ws_browser": "https://narrative.kbase.us/#ws" + "ws_browser": "https://narrative.kbase.us/#ws", + "google_analytics_id": "UA-137652528-1", + "google_ad_id": "AW-753507180", + "google_ad_conversion": "kR9OCLas4JgBEOy2pucC" }, "comm_wait_timeout": 600000, + "config": "dev", "data_panel": { "initial_sort_limit": 10000, "max_name_length": 33, @@ -109,6 +116,7 @@ "ws_max_objs_to_fetch": 30000 }, "dev": { + "KBaseSearchEngine": "https://ci.kbase.us/services/searchapi2/legacy", "auth": "https://ci.kbase.us/services/auth", "awe": "https://ci.kbase.us/services/awe-api", "catalog": "https://ci.kbase.us/services/catalog", @@ -116,6 +124,7 @@ "compound_img_url": "http://minedatabase.mcs.anl.gov/compound_images/ModelSEED/", "data_import_export": "https://ci.kbase.us/services/data_import_export", "data_panel_sources": "/data_source_config.json", + "execution_engine2": "https://ci.kbase.us/services/ee2", "fba": "https://ci.kbase.us/services/KBaseFBAModeling/", "ftp_api_root": "/data/bulk", "ftp_api_url": "https://ci.kbase.us/services/kb-ftp-api/v0", @@ -124,9 +133,7 @@ "groups": "https://ci.kbase.us/services/groups", "invocation": "", "job_service": "https://ci.kbase.us/services/njs_wrapper", - "KBaseSearchEngine": "https://ci.kbase.us/services/searchapi2/legacy", "landing_pages": "/#dataview/", - "provenance_view": "/#objgraphview", "log_host": null, "log_port": null, "log_proxy_host": "172.17.0.1", @@ -136,6 +143,7 @@ "narrative_method_store_image": "https://ci.kbase.us/services/narrative_method_store/", "profile_page": "/#people/", "protein_info": "", + "provenance_view": "/#objgraphview", "reset_password": "https://gologin.kbase.us/ResetPassword", "search": "https://ci.kbase.us/services/search/getResults", "service_wizard": "https://ci.kbase.us/services/service_wizard", @@ -153,14 +161,16 @@ "user_profile": "https://ci.kbase.us/services/user_profile/rpc", "version_check": "/narrative_version", "workspace": "https://ci.kbase.us/services/ws", - "ws_browser": "https://narrative.kbase.us/#ws" + "ws_browser": "https://narrative.kbase.us/#ws", + "google_analytics_id": "UA-74532036-1" }, - "dev_mode": true, - "git_commit_hash": "8928919d6", - "git_commit_time": "Tue Jan 9 11:48:43 2018 -0800", + "dev_mode": true, + "git_commit_hash": "465a86a62", + "git_commit_time": "Fri May 22 16:29:47 2020 -0700", "loading_gif": "/narrative/static/kbase/images/ajax-loader.gif", "name": "KBase Narrative", "next": { + "KBaseSearchEngine": "https://next.kbase.us/services/searchapi2/legacy", "auth": "https://next.kbase.us/services/auth", "awe": "https://next.kbase.us/services/awe-api", "catalog": "https://next.kbase.us/services/catalog", @@ -168,6 +178,7 @@ "compound_img_url": "http://minedatabase.mcs.anl.gov/compound_images/ModelSEED/", "data_import_export": "https://next.kbase.us/services/data_import_export", "data_panel_sources": "/data_source_config.json", + "execution_engine2": "https://next.kbase.us/services/ee2", "fba": "https://next.kbase.us/services/KBaseFBAModeling/", "ftp_api_root": "/data/bulk", "ftp_api_url": "https://next.kbase.us/services/kb-ftp-api/v0", @@ -176,25 +187,24 @@ "groups": "https://next.kbase.us/services/groups", "invocation": "", "job_service": "https://next.kbase.us/services/njs_wrapper", - "KBaseSearchEngine": "https://next.kbase.us/services/searchapi2/legacy", "landing_pages": "/#dataview/", - "provenance_view": "/#objgraphview", - "log_proxy_host": "172.17.42.1", - "log_proxy_port": 32001, "log_host": "https://elasticsearch2.chicago.kbase.us", "log_port": 9000, + "log_proxy_host": "172.17.42.1", + "log_proxy_port": 32001, "login": "https://next.kbase.us/services/authorization/Sessions/Login", "narrative_job_proxy": "http://narrative-next.kbase.us:7068", "narrative_method_store": "https://next.kbase.us/services/narrative_method_store/rpc", "narrative_method_store_image": "https://next.kbase.us/services/narrative_method_store/", "profile_page": "/#people/", "protein_info": "", + "provenance_view": "/#objgraphview", "reset_password": "https://gologin.kbase.us/ResetPassword", "search": "https://kbase.us/services/search/getResults", "service_wizard": "https://next.kbase.us/services/service_wizard", "shock": "https://next.kbase.us/services/shock-api", "staging_api_url": "https://next.kbase.us/services/staging_service", - "static_narrative_root": "https://next.kbase.us/n", + "static_narrative_root": "https://next.kbase.us/sn", "status_page": "http://kbase.us/internal/status/", "submit_jira_ticket": "https://atlassian.kbase.us/secure/CreateIssueDetails!init.jspa?pid=10200&issuetype=1&description=Narrative%20version", "transform": "https://next.kbase.us/services/transform", @@ -206,9 +216,11 @@ "user_profile": "https://next.kbase.us/services/user_profile/rpc", "version_check": "/narrative_version", "workspace": "https://next.kbase.us/services/ws", - "ws_browser": "https://narrative.kbase.us/#ws" + "ws_browser": "https://narrative.kbase.us/#ws", + "google_analytics_id": "UA-74530365-1" }, "prod": { + "KBaseSearchEngine": "https://kbase.us/services/searchapi2/legacy", "auth": "https://kbase.us/services/auth", "awe": "https://kbase.us/services/awe-api", "catalog": "https://kbase.us/services/catalog", @@ -216,6 +228,7 @@ "compound_img_url": "http://minedatabase.mcs.anl.gov/compound_images/ModelSEED/", "data_import_export": "https://kbase.us/services/data_import_export", "data_panel_sources": "/data_source_config.json", + "execution_engine2": "https://kbase.us/services/ee2", "fba": "https://kbase.us/services/KBaseFBAModeling/", "ftp_api_root": "/data/bulk", "ftp_api_url": "https://kbase.us/services/kb-ftp-api/v0", @@ -224,19 +237,18 @@ "groups": "https://kbase.us/services/groups", "invocation": "https://kbase.us/services/invocation", "job_service": "https://kbase.us/services/njs_wrapper", - "KBaseSearchEngine": "https://kbase.us/services/searchapi2/legacy", "landing_pages": "/#dataview/", - "provenance_view": "/#objgraphview", - "log_proxy_host": "172.17.42.1", - "log_proxy_port": 32001, "log_host": "https://elasticsearch2.chicago.kbase.us", "log_port": 9000, + "log_proxy_host": "172.17.42.1", + "log_proxy_port": 32001, "login": "https://kbase.us/services/authorization/Sessions/Login", "narrative_job_proxy": "http://narrative.kbase.us:7068", "narrative_method_store": "https://kbase.us/services/narrative_method_store/rpc", "narrative_method_store_image": "https://kbase.us/services/narrative_method_store/", "profile_page": "/#people/", "protein_info": "https://kbase.us/services/protein_info_service", + "provenance_view": "/#objgraphview", "reset_password": "https://gologin.kbase.us/ResetPassword", "search": "https://kbase.us/services/search/getResults", "service_wizard": "https://kbase.us/services/service_wizard", @@ -254,7 +266,60 @@ "user_profile": "https://kbase.us/services/user_profile/rpc", "version_check": "/narrative_version", "workspace": "https://kbase.us/services/ws", - "ws_browser": "https://narrative.kbase.us/#ws" + "ws_browser": "https://narrative.kbase.us/#ws", + "google_analytics_id": "UA-137652528-1", + "google_ad_id": "AW-753507180", + "google_ad_conversion": "kR9OCLas4JgBEOy2pucC" + }, + "narrative-dev": { + "KBaseSearchEngine": "https://narrative-dev.kbase.us/services/searchapi2/legacy", + "auth": "https://narrative-dev.kbase.us/services/auth", + "awe": "https://narrative-dev.kbase.us/services/awe-api", + "catalog": "https://narrative-dev.kbase.us/services/catalog", + "cdn": "https://narrative-dev.kbase.us/cdn/files", + "compound_img_url": "http://minedatabase.mcs.anl.gov/compound_images/ModelSEED/", + "data_import_export": "https://narrative-dev.kbase.us/services/data_import_export", + "data_panel_sources": "/data_source_config.json", + "execution_engine2": "https://narrative-dev.kbase.us/services/ee2", + "fba": "https://narrative-dev.kbase.us/services/KBaseFBAModeling/", + "ftp_api_root": "/data/bulk", + "ftp_api_url": "https://narrative-dev.kbase.us/services/kb-ftp-api/v0", + "gene_families": "https://narrative-dev.kbase.us/services/gene_families", + "genomeCmp": "https://narrative-dev.kbase.us/services/genome_comparison/jsonrpc", + "groups": "https://narrative-dev.kbase.us/services/groups", + "job_service": "https://narrative-dev.kbase.us/services/njs_wrapper", + "landing_pages": "/#dataview/", + "log_host": "https://elasticsearch2.chicago.kbase.us", + "log_port": 9000, + "log_proxy_host": "172.17.42.1", + "log_proxy_port": 32001, + "login": "https://narrative-dev.kbase.us/services/authorization/Sessions/Login", + "narrative_job_proxy": "https://narrative-dev.kbase.us/services/narrativejobproxy/", + "narrative_method_store": "https://narrative-dev.kbase.us/services/narrative_method_store/rpc", + "narrative_method_store_image": "https://narrative-dev.kbase.us/services/narrative_method_store/", + "narrative_method_store_types": "https://narrative-dev.kbase.us/services/narrative_method_store/rpc", + "profile_page": "/#people/", + "protein_info": "", + "provenance_view": "/#objgraphview", + "reset_password": "https://gologin.kbase.us/ResetPassword", + "search": "https://narrative-dev.kbase.us/services/search/getResults", + "service_wizard": "https://narrative-dev.kbase.us/services/service_wizard", + "shock": "https://narrative-dev.kbase.us/services/shock-api", + "staging_api_url": "https://narrative-dev.kbase.us/services/staging_service", + "static_narrative_root": "https://kbase.us/n", + "status_page": "http://kbase.us/internal/status/", + "submit_jira_ticket": "https://atlassian.kbase.us/secure/CreateIssueDetails!init.jspa?pid=10200&issuetype=1&description=Narrative%20version", + "transform": "https://narrative-dev.kbase.us/services/transform", + "trees": "https://narrative-dev.kbase.us/services/trees", + "ui_common_root": "https://narrative-dev.kbase.us/", + "update_profile": "https://gologin.kbase.us/account/UpdateProfile", + "uploader": "", + "user_and_job_state": "https://narrative-dev.kbase.us/services/userandjobstate", + "user_profile": "https://narrative-dev.kbase.us/services/user_profile/rpc", + "version_check": "/narrative_version", + "workspace": "https://narrative-dev.kbase.us/services/ws", + "ws_browser": "https://narrative-dev.kbase.us/#ws", + "google_analytics_id": "UA-131054609-1" }, "release_notes": "https://github.com/kbase/narrative/blob/master/RELEASE_NOTES.md", "tooltip": { @@ -262,5 +327,5 @@ "showDelay": 750 }, "use_local_widgets": true, - "version": "4.1.2" + "version": "4.2.0" } diff --git a/src/config.json.templ b/src/config.json.templ index 25dfa76aec..d47224f3af 100644 --- a/src/config.json.templ +++ b/src/config.json.templ @@ -8,6 +8,7 @@ "compound_img_url": "http://minedatabase.mcs.anl.gov/compound_images/ModelSEED/", "data_import_export": "https://appdev.kbase.us/services/data_import_export", "data_panel_sources": "/data_source_config.json", + "execution_engine2": "https://appdev.kbase.us/services/ee2", "fba": "https://appdev.kbase.us/services/KBaseFBAModeling/", "ftp_api_root": "/data/bulk", "ftp_api_url": "https://appdev.kbase.us/services/kb-ftp-api/v0", @@ -59,6 +60,7 @@ "compound_img_url": "http://minedatabase.mcs.anl.gov/compound_images/ModelSEED/", "data_import_export": "https://ci.kbase.us/services/data_import_export", "data_panel_sources": "/data_source_config.json", + "execution_engine2": "https://ci.kbase.us/services/ee2", "fba": "https://ci.kbase.us/services/KBaseFBAModeling/", "ftp_api_root": "/data/bulk", "ftp_api_url": "https://ci.kbase.us/services/kb-ftp-api/v0", @@ -122,6 +124,7 @@ "compound_img_url": "http://minedatabase.mcs.anl.gov/compound_images/ModelSEED/", "data_import_export": "https://ci.kbase.us/services/data_import_export", "data_panel_sources": "/data_source_config.json", + "execution_engine2": "https://ci.kbase.us/services/ee2", "fba": "https://ci.kbase.us/services/KBaseFBAModeling/", "ftp_api_root": "/data/bulk", "ftp_api_url": "https://ci.kbase.us/services/kb-ftp-api/v0", @@ -162,8 +165,8 @@ "google_analytics_id": "UA-74532036-1" }, "dev_mode": {{ if ne .Env.CONFIG_ENV "prod" }} true {{- else }} false {{- end }}, - "git_commit_hash": "2364f63f4", - "git_commit_time": "Tue Nov 12 11:34:01 2019 -0800", + "git_commit_hash": "465a86a62", + "git_commit_time": "Fri May 22 16:29:47 2020 -0700", "loading_gif": "/narrative/static/kbase/images/ajax-loader.gif", "name": "KBase Narrative", "next": { @@ -175,6 +178,7 @@ "compound_img_url": "http://minedatabase.mcs.anl.gov/compound_images/ModelSEED/", "data_import_export": "https://next.kbase.us/services/data_import_export", "data_panel_sources": "/data_source_config.json", + "execution_engine2": "https://next.kbase.us/services/ee2", "fba": "https://next.kbase.us/services/KBaseFBAModeling/", "ftp_api_root": "/data/bulk", "ftp_api_url": "https://next.kbase.us/services/kb-ftp-api/v0", @@ -224,6 +228,7 @@ "compound_img_url": "http://minedatabase.mcs.anl.gov/compound_images/ModelSEED/", "data_import_export": "https://kbase.us/services/data_import_export", "data_panel_sources": "/data_source_config.json", + "execution_engine2": "https://kbase.us/services/ee2", "fba": "https://kbase.us/services/KBaseFBAModeling/", "ftp_api_root": "/data/bulk", "ftp_api_url": "https://kbase.us/services/kb-ftp-api/v0", @@ -275,6 +280,7 @@ "compound_img_url": "http://minedatabase.mcs.anl.gov/compound_images/ModelSEED/", "data_import_export": "https://narrative-dev.kbase.us/services/data_import_export", "data_panel_sources": "/data_source_config.json", + "execution_engine2": "https://narrative-dev.kbase.us/services/ee2", "fba": "https://narrative-dev.kbase.us/services/KBaseFBAModeling/", "ftp_api_root": "/data/bulk", "ftp_api_url": "https://narrative-dev.kbase.us/services/kb-ftp-api/v0", @@ -321,5 +327,5 @@ "showDelay": 750 }, "use_local_widgets": true, - "version": "4.1.2" + "version": "4.2.0" } diff --git a/src/scripts/py2_code_hunter/narr_info.py b/src/scripts/py2_code_hunter/narr_info.py index 971cede4cd..d59ce6cd69 100644 --- a/src/scripts/py2_code_hunter/narr_info.py +++ b/src/scripts/py2_code_hunter/narr_info.py @@ -15,7 +15,7 @@ def add_updated_cell(self, idx:int, original_source:str, updated_source:str): if different, mark them and add an updated cell to the count """ if original_source != updated_source: - self.changed_cells[idx] = CellChange(idx, original_source, updated_source).to_dict() + self.changed_cells[idx] = CellChange(self.ws_id, idx, original_source, updated_source).to_dict() self.updated_cells += 1 def to_dict(self): @@ -31,18 +31,29 @@ def __repr__(self): return json.dumps(self.to_dict()) class CellChange(object): - def __init__(self, idx: int, original: str, updated: str): + def __init__(self, ws_id: int, idx: int, original: str, updated: str): + self.ws_id = ws_id self.updated_lines = {} self.original = original self.updated = updated + self.cell_idx = idx self._init_lines(original, updated) def _init_lines(self, original: str, updated: str): orig_lines = original.split("\n") updated_lines = updated.split("\n") - for idx, line in enumerate(orig_lines): - if line != updated_lines[idx]: - self.updated_lines[idx] = {"o": line, "u": updated_lines[idx]} + if len(orig_lines) != len(updated_lines): + print(f"WS:{self.ws_id} updating cell {self.cell_idx} - can't compare line by line, squashing them all together.") + self.updated_lines = { + 0: { + "o": original, + "u": updated + } + } + else: + for idx, line in enumerate(orig_lines): + if line != updated_lines[idx]: + self.updated_lines[idx] = {"o": line, "u": updated_lines[idx]} def __repr__(self): return json.dumps(self.to_dict()) diff --git a/test/unit/karma.conf.js b/test/unit/karma.conf.js index 9e4a69e87b..e8a8482fcc 100644 --- a/test/unit/karma.conf.js +++ b/test/unit/karma.conf.js @@ -30,8 +30,6 @@ module.exports = function (config) { files: [ 'kbase-extension/static/narrative_paths.js', {pattern: 'test/unit/spec/**/*.js', included: false}, - // {pattern: 'test/unit/spec/appWidgets/input/taxonomyRefInputSpec.js', included: false}, - // {pattern: 'test/unit/spec/common/validate-Spec.js', included: false}, {pattern: 'node_modules/string.prototype.startswith/startswith.js', included: true}, {pattern: 'node_modules/string.prototype.endswith/endswith.js', included: true}, {pattern: 'node_modules/jasmine-ajax/lib/mock-ajax.js', included: true}, diff --git a/test/unit/spec/Util/jobLogViewerSpec.js b/test/unit/spec/Util/jobLogViewerSpec.js index 6443be6081..44a1da0ad5 100644 --- a/test/unit/spec/Util/jobLogViewerSpec.js +++ b/test/unit/spec/Util/jobLogViewerSpec.js @@ -94,7 +94,7 @@ define([ { jobId: jobId, jobState: { - job_state: 'in-progress' + status: 'running' } }, { @@ -126,5 +126,155 @@ define([ }); viewer.detach(); }); + + it('Should render on job-logs messages immediately on startup', (done) => { + let viewer = JobLogViewer.make(); + const jobId = 'testJobLogMsgResp'; + const arg = { + node: hostNode, + jobId: jobId + }; + runtimeBus.on('request-job-status', (msg) => { + expect(msg).toEqual({jobId: jobId}); + runtimeBus.send( + { + jobId: jobId, + jobState: { + status: 'running' + } + }, + { + channel: { + jobId: jobId + }, + key: { + type: 'job-status' + } + } + ); + }); + + runtimeBus.on('request-latest-job-log', (msg) => { + expect(msg).toEqual({jobId: jobId, options: {}}); + runtimeBus.send( + { + jobId: jobId, + latest: true, + logs: { + first: 0, + job_id: jobId, + latest: true, + max_lines: 2, + lines: [{ + is_error: 0, + line: 'line 1 - log', + linepos: 1, + ts: 123456789 + }, { + is_error: 1, + line: 'line 2 - error', + linepos: 1, + ts: 123456790 + }] + } + }, + { + channel: { + jobId: jobId + }, + key: { + type: 'job-logs' + } + } + ); + setTimeout(() => { + const panel = hostNode.querySelector('[data-element="log-panel"]'); + expect(panel.children.length).toEqual(2); + const logLine = panel.children[0]; + expect(logLine.classList.toLocaleString()).toEqual('kblog-line'); + expect(logLine.innerHTML).toContain('line 1 - log'); + const errorLine = panel.children[1]; + expect(errorLine.classList.toLocaleString()).toEqual('kblog-line kb-error'); + expect(errorLine.innerHTML).toContain('line 2 - error'); + viewer.detach(); + done(); + }, 500); + }); + viewer.start(arg); + }); + + it('Should render a queued message for queued jobs', (done) => { + let viewer = JobLogViewer.make(); + const jobId = 'testJobQueued'; + const arg = { + node: hostNode, + jobId: jobId + }; + runtimeBus.on('request-job-status', (msg) => { + expect(msg).toEqual({jobId: jobId}); + runtimeBus.send( + { + jobId: jobId, + jobState: { + status: 'queued' + } + }, + { + channel: { + jobId: jobId + }, + key: { + type: 'job-status' + } + } + ); + }); + runtimeBus.on('request-job-update', (msg) => { + expect(msg).toEqual({jobId: jobId}); + runtimeBus.send( + { + jobId: jobId, + jobState: { + status: 'queued' + } + }, + { + channel: { + jobId: jobId + }, + key: { + type: 'job-status' + } + } + ); + setTimeout(() => { + const panel = hostNode.querySelector('[data-element="log-panel"]'); + console.log(panel); + expect(panel.children.length).toEqual(1); + expect(panel.children[0].innerHTML).toContain('Job is queued'); //, logs will be available when the job is running.'); + done(); + }, 500); + }); + viewer.start(arg); + }); + + xit('Should render a canceled message for canceled jobs', (done) => { + }); + + xit('Should render an error message for errored jobs', (done) => { + + }); + + xit('Should have the top button go to the top', (done) => { + + }); + + xit('Should have the bottom button go to the end', (done) => { + + }); + + xit('Should have the stop button make sure it stops', (done) => { + + }); }); }) diff --git a/test/unit/spec/api/authSpec.js b/test/unit/spec/api/authSpec.js new file mode 100644 index 0000000000..3e8f922a34 --- /dev/null +++ b/test/unit/spec/api/authSpec.js @@ -0,0 +1,370 @@ +/*global define*/ +/*global describe, it, expect*/ +/*global jasmine, pending*/ +/*global beforeEach, afterEach*/ +/*jslint white: true*/ + +define([ + 'api/auth', + 'narrativeConfig', + 'testUtil', + 'uuid' +], function ( + Auth, + Config, + TestUtil, + Uuid +) { + 'use strict'; + + let authClient, + token; + + // The following functions ensure that the token for the "user" configured + // in test/unit/testConfig.json is set in the standard session cookie field + // before each test, and removed afterwards. + // + // This is for the convenience of testing "auth" functions which assume + // existing authentication, yet because it doesn't use the auth cookie functions, + // which is arguably beneficial here as it does keeps the scope of test setup + // more limited in scope. + // + // Note that for tests which test the auth cookie functions, clearToken() + // must be called first to clear out the session cookie. + // + // Also note that this does not deal with the backup or narrative session cookies, + // since those are + const cookieKeys = ['kbase_session']; + + function setToken(token) { + cookieKeys.forEach((key) => { + document.cookie = `${key}=${token}`; + }); + } + + function clearToken() { + cookieKeys.forEach((key) => { + document.cookie = `${key}=`; + }); + } + + describe('Test the Auth API module', () => { + beforeEach(() => { + token = TestUtil.getAuthToken(); + setToken(token); + authClient = Auth.make({ + url: Config.url('auth'), + // Can't use secure cookies for testing. + secureCookies: false + }); + }); + + afterEach(() => { + clearToken(); + }); + + it('Should make a new Auth client on request', () => { + var auth = Auth.make({url: Config.url('auth')}); + expect(auth).not.toBeNull(); + }); + + it('Should get auth token info', (done) => { + TestUtil.pendingIfNoToken(); + + authClient.getTokenInfo(token) + .then((response) => { + expect(Object.keys(response)).toContain('expires'); + done(); + }) + .catch((error) => { + expect(error).toBeNull(); + done(); + }); + }); + + it('Should fail to get auth token info from a fake token', (done) => { + TestUtil.pendingIfNoToken(); + + authClient.getTokenInfo('faketoken') + .then((response) => { + expect(response).toBeNull(); + done(); + }) + .catch((error) => { + expect(error).not.toBeNull(); + expect(Object.keys(error)).toContain('error'); + done(); + }); + }); + + it('Should return my profile', (done) => { + TestUtil.pendingIfNoToken(); + + authClient.getUserProfile() + .then((response) => { + expect(response).not.toBeNull(); + expect(Object.keys(response)).toContain('display'); + done(); + }) + .catch((error) => { + expect(error).toBeNull(); + done(); + }); + }); + + it('Should fail to get profile with a missing token', (done) => { + clearToken(); + authClient.getCurrentProfile() + .then(() => { + done.fail('Should have failed!'); + }) + .catch(() => { + done(); + }); + }); + + it('Should fail to get profile with a bad token', (done) => { + clearToken(); + setToken('someBadToken'); + authClient.getCurrentProfile() + .then(() => { + done.fail('Should have failed!'); + }) + .catch(() => { + done(); + }); + }); + + it('Should return a list of users', (done) => { + TestUtil.pendingIfNoToken(); + + var badName = 'not_a_user_name'; + authClient.getUserNames(token, ['wjriehl', badName]) + .then((names) => { + expect(names.wjriehl).toEqual('William Riehl'); + expect(Object.keys(names)).not.toContain(badName); + done(); + }) + .catch((error) => { + expect(error).toBeNull(); + done(); + }); + }); + + it('Should fail to get users with a bad token', (done) => { + const token = 'someBadToken'; + clearToken(); + setToken(token); + authClient.getUserNames(token, ['wjriehl']) + .then(() => { + done.fail('Should have failed!'); + }) + .catch(() => { + done(); + }); + }); + + it('Should fail to get users with a missing token', (done) => { + clearToken(); + authClient.getUserNames(null, ['wjriehl']) + .then(() => { + done.fail('Should have failed!'); + }) + .catch(() => { + done(); + }); + }); + + it('Should search for users', (done) => { + TestUtil.pendingIfNoToken(); + + var query = 'ie'; + authClient.searchUserNames(token, query) + .then((results) => { + expect(results).not.toBeNull(); + done(); + }) + .catch((error) => { + expect(error).toBeNull(); + done(); + }); + }); + + it('Should search for users with extra options', (done) => { + TestUtil.pendingIfNoToken(); + + var query = 'ie'; + var options = ['']; + authClient.searchUserNames(token, query, options) + .then((results) => { + expect(results).not.toBeNull(); + done(); + }) + .catch((error) => { + expect(error).toBeNull(); + done(); + }); + }); + + it('Should fail to search for users with a bad token', (done) => { + authClient.searchUserNames('someBadToken', 'ie', ['']) + .then(() => { + done.fail('Should have failed!'); + }) + .catch(() => { + done(); + }); + }); + + it('Should fail to search for users with a missing token', (done) => { + clearToken(); + authClient.searchUserNames(null, 'ie', ['']) + .then(() => { + done.fail('Should have failed!'); + }) + .catch(() => { + done(); + }); + }); + + it('Should set an auth token cookie', () => { + clearToken(); + const newToken = 'someRandomToken'; + authClient.setAuthToken(newToken); + expect(authClient.getAuthToken()).toEqual(newToken); + }); + + it('Should validate an auth token on request', (done) => { + let doneCount = 0; + const trials = [{ + token: null, + isValid: true // should get it from cookie + }, { + token: 'someRandomToken', + isValid: false + }]; + trials.forEach((trial) => { + authClient.validateToken(trial.token) + .then((isValid) => { + expect(isValid).toBe(trial.isValid); + doneCount++; + if (doneCount === trials.length) { + done(); + } + }) + .catch(() => { + done(); + }); + }); + }); + + it('Should clear auth token cookie on request', () => { + // Ensure that the token automagically set is removed first. + clearToken(); + + // Setting an arbitrary token should work. + const cookieValue = new Uuid(4).format(); + authClient.setAuthToken(cookieValue); + expect(authClient.getAuthToken()).toEqual(cookieValue); + + // Clearing an auth token should also work. + authClient.clearAuthToken(); + expect(authClient.getAuthToken()).toBeNull(); + + // Just to be totally sure. + expect(authClient.getCookie('kbase_session')).toBeNull(); + }); + + it('Should properly handle backup cookie in non-prod environment', () => { + const env = Config.get('environment'); + const backupCookieName = 'kbase_session_backup'; + if (env === 'prod') { + pending('This test is not valid for a prod config'); + return; + } + + // Ensure that the token automagically set is removed first. + clearToken(); + + // Get a unique fake token, to ensure we don't conflict with + // another cookie value. + const cookieValue = new Uuid(4).format(); + + authClient.setAuthToken(cookieValue); + expect(authClient.getAuthToken()).toEqual(cookieValue); + + // There should not be a backup cookie set yet + expect(authClient.getCookie(backupCookieName)).toBeNull(); + + // Since the backup cookie is only set in prod, we simulate the backup + // cookie having been set in prod in another session. + const backupCookieValue = new Uuid(4).format(); + + // Note the domain of localhost -- we can't use the real kbase.us domain. + authClient.setCookie({ + name: backupCookieName, + value: backupCookieValue, + domain: 'localhost' + }); + + // The backup cookie should be set. + expect(authClient.getCookie(backupCookieName)).toEqual(backupCookieValue); + + // Clearing an auth token should also work. + authClient.clearAuthToken(); + expect(authClient.getAuthToken()).toBeNull(); + expect(authClient.getCookie('kbase_session')).toBeNull(); + expect(authClient.getCookie(backupCookieName)).toEqual(backupCookieValue); + }); + + it('Should set and clear backup cookie in prod', () => { + const env = Config.get('environment'); + const backupCookieName = 'kbase_session_backup'; + if (env !== 'prod') { + pending('This test is only valid for a prod config'); + return; + } + + // Ensure that the token automagically set is removed first. + clearToken(); + + // Setting an arbitrary token should work. + const cookieValue = new Uuid(4).format(); + authClient.setAuthToken(cookieValue); + expect(authClient.getAuthToken()).toEqual(cookieValue); + expect(authClient.getCookie(backupCookieName)).toEqual(cookieValue); + + // Clearing an auth token should also work. + authClient.clearAuthToken(); + expect(authClient.getAuthToken()).toBeNull(); + expect(authClient.getCookie('kbase_session')).toBeNull(); + expect(authClient.getCookie(backupCookieName)).toBeNull(); + }); + + it('Should clear the narrative_session cookie when the auth cookie is cleared', () => { + // Ensure that the token automagically set is removed first. + clearToken(); + + // We'll simulate the narrative_session token. + // This token is not set by the Narrative, but Traefik. + const cookieValue = new Uuid(4).format(); + authClient.setCookie({ + name: 'narrative_session', + value: cookieValue, + expires: 14 + }); + + // Okay, it should be set. + expect(authClient.getCookie('narrative_session')).toEqual(cookieValue); + + // Clearing the auth token should zap the narrative_session cookie too. + authClient.clearAuthToken(); + expect(authClient.getAuthToken()).toBeNull(); + expect(authClient.getCookie('narrative_session')).toBeNull(); + }); + + it('Should revoke an auth token on request', () => { + // This deletes the token. Should be mocked? + }); + }); +}); diff --git a/test/unit/spec/appWidgets/input/checkboxInputSpec.js b/test/unit/spec/appWidgets/input/checkboxInputSpec.js index f73d13d013..714d8d1015 100644 --- a/test/unit/spec/appWidgets/input/checkboxInputSpec.js +++ b/test/unit/spec/appWidgets/input/checkboxInputSpec.js @@ -16,10 +16,11 @@ define([ describe('Test checkbox data input widget', function() { let testConfig = {}, - runtime = Runtime.make(), + runtime, bus; beforeEach(function() { + runtime = Runtime.make(); bus = runtime.bus().makeChannelBus({ description: 'checkbox testing' }); diff --git a/test/unit/spec/appWidgets/input/floatInputSpec.js b/test/unit/spec/appWidgets/input/floatInputSpec.js index 22a5d6fa8f..411b274809 100644 --- a/test/unit/spec/appWidgets/input/floatInputSpec.js +++ b/test/unit/spec/appWidgets/input/floatInputSpec.js @@ -12,10 +12,11 @@ define([ describe('Test float data input widget', function() { let testConfig = {}, - runtime = Runtime.make(), + runtime, bus; beforeEach(function() { + runtime = Runtime.make(); bus = runtime.bus().makeChannelBus({ description: 'float testing', // name: 'float-test-' + Math.floor(Math.random()*10000) diff --git a/test/unit/spec/appWidgets/input/intInputSpec.js b/test/unit/spec/appWidgets/input/intInputSpec.js index 01df6e4c72..15f096af68 100644 --- a/test/unit/spec/appWidgets/input/intInputSpec.js +++ b/test/unit/spec/appWidgets/input/intInputSpec.js @@ -12,10 +12,11 @@ define([ describe('Test int data input widget', function() { let testConfig = {}, - runtime = Runtime.make(), + runtime, bus; beforeEach(function() { + runtime = Runtime.make(); bus = runtime.bus().makeChannelBus({ description: 'int input testing', }); diff --git a/test/unit/spec/appWidgets/input/newObjectInputSpec.js b/test/unit/spec/appWidgets/input/newObjectInputSpec.js index 4dcccc99b7..b795c3d753 100644 --- a/test/unit/spec/appWidgets/input/newObjectInputSpec.js +++ b/test/unit/spec/appWidgets/input/newObjectInputSpec.js @@ -15,7 +15,7 @@ define([ let bus, testConfig, required = false, - runtime = Runtime.make(), + runtime, node, defaultValue = 'apple'; const wsObjName = 'SomeObject', @@ -48,6 +48,7 @@ define([ describe('New Object Input tests', () => { beforeEach(() => { + runtime = Runtime.make(); if (TestUtil.getAuthToken()) { document.cookie = 'kbase_session=' + TestUtil.getAuthToken(); Jupyter.narrative = new Narrative(); @@ -57,7 +58,7 @@ define([ node = document.createElement('div'); bus = runtime.bus().makeChannelBus({ - description: 'select input testing', + description: 'select input testing - ' + Math.random().toString(36).substring(2) }); testConfig = buildTestConfig(required, defaultValue, bus); diff --git a/test/unit/spec/appWidgets/input/select2ObjectinputSpec.js b/test/unit/spec/appWidgets/input/select2ObjectinputSpec.js index 3a6d84d659..7e8ac11be7 100644 --- a/test/unit/spec/appWidgets/input/select2ObjectinputSpec.js +++ b/test/unit/spec/appWidgets/input/select2ObjectinputSpec.js @@ -24,7 +24,7 @@ define([ ], dummyData = [readsItem, readsItem2], dummyObjInfo = [objectify(readsItem), objectify(readsItem2)]; - let runtime = Runtime.make(); + let runtime; function objectify(infoArr) { let splitType = infoArr[2].split('-'); @@ -90,6 +90,7 @@ define([ fakeServiceUrl = 'https://ci.kbase.us/services/fake_taxonomy_service'; beforeEach(() => { + runtime = Runtime.make(); if (TestUtil.getAuthToken()) { document.cookie = 'kbase_session=' + TestUtil.getAuthToken(); Jupyter.narrative = new Narrative(); diff --git a/test/unit/spec/appWidgets/input/selectInputSpec.js b/test/unit/spec/appWidgets/input/selectInputSpec.js index 3b1bb2035e..a1f49c970c 100644 --- a/test/unit/spec/appWidgets/input/selectInputSpec.js +++ b/test/unit/spec/appWidgets/input/selectInputSpec.js @@ -11,7 +11,7 @@ define([ let bus, testConfig, required = false, - runtime = Runtime.make(), + runtime, node, defaultValue = 'apple'; @@ -45,6 +45,7 @@ define([ describe('Select Input tests', () => { beforeEach(() => { + runtime = Runtime.make(); node = document.createElement('div'); bus = runtime.bus().makeChannelBus({ description: 'select input testing', diff --git a/test/unit/spec/appWidgets/input/taxonomyRefInputSpec.js b/test/unit/spec/appWidgets/input/taxonomyRefInputSpec.js index ad4d12af84..5ed354e544 100644 --- a/test/unit/spec/appWidgets/input/taxonomyRefInputSpec.js +++ b/test/unit/spec/appWidgets/input/taxonomyRefInputSpec.js @@ -38,13 +38,14 @@ define([ let bus, testConfig, required = false, - runtime = Runtime.make(), + runtime, node, defaultValue = 'apple', fakeServiceUrl = 'https://ci.kbase.us/services/fake_taxonomy_service'; beforeEach(() => { + runtime = Runtime.make(); if (TestUtil.getAuthToken()) { document.cookie = 'kbase_session=' + TestUtil.getAuthToken(); Jupyter.narrative = new Narrative(); diff --git a/test/unit/spec/appWidgets/input/textInputSpec.js b/test/unit/spec/appWidgets/input/textInputSpec.js index cc62779934..bd816e333d 100644 --- a/test/unit/spec/appWidgets/input/textInputSpec.js +++ b/test/unit/spec/appWidgets/input/textInputSpec.js @@ -11,7 +11,7 @@ define([ let bus, testConfig, required = false, - runtime = Runtime.make(), + runtime, node, defaultValue = 'some test text'; @@ -34,6 +34,7 @@ define([ describe('Text Input tests', () => { beforeEach(() => { + runtime = Runtime.make(); node = document.createElement('div'); bus = runtime.bus().makeChannelBus({ description: 'text input testing', diff --git a/test/unit/spec/appWidgets/input/textareaInputSpec.js b/test/unit/spec/appWidgets/input/textareaInputSpec.js index 61633a2243..7416c64597 100644 --- a/test/unit/spec/appWidgets/input/textareaInputSpec.js +++ b/test/unit/spec/appWidgets/input/textareaInputSpec.js @@ -9,7 +9,7 @@ define([ let bus, testConfig, required = false, - runtime = Runtime.make(), + runtime, node, defaultValue = 'some test text', numRows = 3; @@ -36,6 +36,7 @@ define([ describe('Textarea Input tests', () => { beforeEach(() => { + runtime = Runtime.make(); node = document.createElement('div'); bus = runtime.bus().makeChannelBus({ description: 'textarea testing', diff --git a/test/unit/spec/appWidgets/input/toggleButtonInputSpec.js b/test/unit/spec/appWidgets/input/toggleButtonInputSpec.js index ac3bdcbc71..83566f7c55 100644 --- a/test/unit/spec/appWidgets/input/toggleButtonInputSpec.js +++ b/test/unit/spec/appWidgets/input/toggleButtonInputSpec.js @@ -15,7 +15,7 @@ define([ let bus, testConfig, required = false, - runtime = Runtime.make(), + runtime, node, defaultValue = true; @@ -39,6 +39,7 @@ define([ describe('ToggleButtonInput tests', () => { beforeEach(() => { + runtime = Runtime.make(); node = document.createElement('div'); bus = runtime.bus().makeChannelBus({ description: 'toggle button testing', diff --git a/test/unit/spec/authSpec.js b/test/unit/spec/authSpec.js deleted file mode 100644 index f403e45f09..0000000000 --- a/test/unit/spec/authSpec.js +++ /dev/null @@ -1,156 +0,0 @@ -/*global define*/ -/*global describe, it, expect*/ -/*global jasmine, pending*/ -/*global beforeEach, afterEach*/ -/*jslint white: true*/ - -define ([ - 'api/auth', - 'narrativeConfig', - 'testUtil' -], function( - Auth, - Config, - TestUtil -) { - 'use strict'; - - var authClient, - token; - - beforeEach(function() { - token = TestUtil.getAuthToken(); - document.cookie='kbase_session=' + token; - authClient = Auth.make({url: Config.url('auth')}); - }); - - describe('Test the Auth API module', function() { - it('Should make a new Auth client on request', function() { - TestUtil.pendingIfNoToken(); - - var auth = Auth.make({url: Config.url('auth')}); - expect(auth).not.toBeNull(); - }); - - it('Should get auth token info', function(done) { - TestUtil.pendingIfNoToken(); - - authClient.getTokenInfo(token) - .then(function(response) { - expect(Object.keys(response)).toContain('expires'); - done(); - }) - .catch(function(error) { - expect(error).toBeNull(); - done(); - }); - }); - - it('Should fail to get auth token info from a fake token', function(done) { - TestUtil.pendingIfNoToken(); - - authClient.getTokenInfo('faketoken') - .then(function(response) { - expect(response).toBeNull(); - done(); - }) - .catch(function(error) { - expect(error).not.toBeNull(); - expect(Object.keys(error)).toContain('error'); - done(); - }); - }); - - it('Should return my profile', function(done) { - TestUtil.pendingIfNoToken(); - - authClient.getUserProfile() - .then(function(response) { - expect(response).not.toBeNull(); - expect(Object.keys(response)).toContain('display'); - done(); - }) - .catch(function(error) { - expect(error).toBeNull(); - done(); - }); - }); - - it('Should fail to get profile with a bad token', function(done) { - // TODO - done(); - }); - - it('Should fail to get profile with an expired token', function(done) { - // TODO - done(); - }); - - it('Should return a list of users', function(done) { - TestUtil.pendingIfNoToken(); - - var badName = 'not_a_user_name'; - authClient.getUserNames(token, ['wjriehl', badName]) - .then(function(names) { - expect(names.wjriehl).toEqual('William Riehl'); - expect(Object.keys(names)).not.toContain(badName); - done(); - }) - .catch(function(error) { - expect(error).toBeNull(); - done(); - }); - }); - - it('Should fail to get users with a bad token', function(done) { - //TODO - done(); - }); - - it('Should fail to get users with an expired token', function(done) { - //TODO - done(); - }); - - it('Should search for users', function(done) { - TestUtil.pendingIfNoToken(); - - var query = 'ie'; - authClient.searchUserNames(token, query) - .then(function(results) { - expect(results).not.toBeNull(); - done(); - }) - .catch(function(error) { - expect(error).toBeNull(); - done(); - }); - }); - - it('Should search for users with extra options', function(done) { - TestUtil.pendingIfNoToken(); - - var query = 'ie'; - var options = ['']; - authClient.searchUserNames(token, query, options) - .then(function(results) { - expect(results).not.toBeNull(); - done(); - }) - .catch(function(error) { - expect(error).toBeNull(); - done(); - }); - }); - - it('Should fail to search for users with a bad token', function(done) { - //TODO - done(); - }); - - it('Should fail to search for users with an expired token', function(done) { - //TODO - done(); - }); - }); -}); diff --git a/test/unit/spec/common/jobsSpec.js b/test/unit/spec/common/jobsSpec.js new file mode 100644 index 0000000000..37befa7aca --- /dev/null +++ b/test/unit/spec/common/jobsSpec.js @@ -0,0 +1,44 @@ +define([ + 'common/jobs' +], (Jobs) => { + describe('Test Jobs module', () => { + it('Should be loaded with the right functions', () => { + expect(Jobs).toBeDefined(); + expect(Jobs.isValidJobState).toBeDefined(); + }); + + it('Should know how to tell good job states', () => { + const goodJs = { + job_id: 'foo', + created: 12345, + other: 'key', + another: 'key' + }; + expect(Jobs.isValidJobState(goodJs)).toBeTrue(); + }); + + it('Should know how to tell bad job states', () => { + const badJsList = [ + 1, + 'foo', + ['a', 'list'], + { + job_id: 'somejob', + other: 'key' + }, + { + created: 'at_some_point', + other: 'key' + }, + { + foobar: 'baz' + }, + null, + undefined + ]; + badJsList.forEach(elem => { + expect(Jobs.isValidJobState(elem)).toBeFalse(); + }); + }); + }); +}); diff --git a/test/unit/spec/function_output/kbaseSampleSet-spec.js b/test/unit/spec/function_output/kbaseSampleSet-spec.js new file mode 100644 index 0000000000..12f7eb086d --- /dev/null +++ b/test/unit/spec/function_output/kbaseSampleSet-spec.js @@ -0,0 +1,135 @@ +/*global define*/ +/*global describe, it, expect*/ +/*global jasmine*/ +/*global beforeEach, afterEach*/ +/*jslint white: true*/ +define([ + 'jquery', + 'kbaseSampleSetView', + 'base/js/namespace', + 'kbaseNarrative', + 'narrativeConfig' +], ( + $, + Widget, + Jupyter, + Narrative, + Config +) => { + describe('Test the kbaseSampleSet viewer widget', () => { + let $div = null; + beforeEach(() => { + jasmine.Ajax.install(); + $div = $('
'); + Jupyter.narrative = new Narrative(); + Jupyter.narrative.getAuthToken = () => { return 'NotARealToken!' }; + }); + + afterEach(() => { + jasmine.Ajax.uninstall(); + $div.remove(); + }); + + it('Should properly render SampleSet', (done) => { + let SampleSet = { + "samples": [ + {'id': "madeup", "name": "sample1"}, + {'id': "idtwo", "name": "sample2"} + ], + "description": "This is a test sample set." + }; + let obj_info = [35,"name","","",1,"",45700] + jasmine.Ajax.stubRequest('https://ci.kbase.us/services/ws').andReturn({ + status: 200, + statusText: 'success', + contentType: 'application/json', + responseHeaders: '', + responseText: JSON.stringify({ + version: '1.1', + result: [{ + data: [ + { + data: SampleSet, + info: obj_info + } + ] + }] + }) + }); + let fakeServiceUrl = "https://ci.kbase.us/services/fake_url"; + var sampleServiceInfo = { + version: '1.1', + id: '12345', + result: [{ + git_commit_hash: "foo", + hash: 'bar', + health: "healthy", + module_name: "SampleService", + url: fakeServiceUrl + }] + } + jasmine.Ajax.stubRequest(Config.url('service_wizard')).andReturn({ + status: 200, + statusText: 'HTTP/1/1 200 OK', + contentType: 'application/json', + responseText: JSON.stringify(sampleServiceInfo), + response: JSON.stringify(sampleServiceInfo), + }); + jasmine.Ajax.stubRequest(fakeServiceUrl).andReturn({ + status: 200, + statusText: 'success', + contentType: 'application/json', + responseHeaders: '', + responseText: JSON.stringify({ + version: '1.1', + result: [{ + sample_id: "id", + user: "user", + node_tree: [{ + id: "identificazione", + parent: null, + type: "sample", + meta_controlled: { + "controlled1": {"value": 1, "units": "bars"}, + "controlled2": {"value": "two", "units": "units"}, + "controlled3": {"value": "3"} + }, + meta_user: { + "user1": {"value": 6, "units": "units"}, + "user2": {"value": "foo"}, + "user3": {"value": "bar"} + } + }], + name: "sample_name", + save_date: "a time", + version: 1 + }] + }) + }); + let w = new Widget($div, {upas: {id: 'fake'}}); + setTimeout(() => { + [ + 'Description', + 'KBase Object Name', + "This is a test sample set." + ].forEach((str) => { + expect($div.html()).toContain(str); + }); + $div.find('a[data-tab="Samples"]').click(); + setTimeout(() => { + [ + "Sample ID", + "Sample Name", + "madeup", + "idtwo", + "two units" + ].forEach((str) => { + expect($div.html()).toContain(str); + }); + done(); + }, 50); + }, 50); + + }); + }); +}); diff --git a/test/unit/spec/narrative_core/jobCommChannel-spec.js b/test/unit/spec/narrative_core/jobCommChannel-spec.js new file mode 100644 index 0000000000..b0ab0bf090 --- /dev/null +++ b/test/unit/spec/narrative_core/jobCommChannel-spec.js @@ -0,0 +1,481 @@ +/*global define*/ +/*global describe, it, expect*/ +/*global jasmine*/ +/*global beforeEach, afterEach*/ +/*jslint white: true*/ +define([ + 'jobCommChannel', + 'base/js/namespace', + 'common/runtime' +], ( + JobCommChannel, + Jupyter, + Runtime +) => { + 'use strict'; + + const DEFAULT_COMM_INFO = { + content: { + comms: [] + } + }; + const DEFAULT_COMM = { + on_msg: () => { }, + send: () => { }, + send_shell_message: () => { } + }; + + /** + * A simple channel "checker" - initializes a bus listener such that the following + * gets passed to bus.listen(): + * { + * channel: { + * channelName: channelId + * }, + * key: { + * type: channelKey + * }, + * handle: cb + * } + * where cb is the callback that gets done as part of the message passing. + * @param {string} channelName + * @param {string} channelId + * @param {string} channelKey + * @param {function} cb + */ + function channelChecker(channelName, channelId, channelKey, cb, msgCmp) { + let channel = {}; + channel[channelName] = channelId; + + Runtime.make().bus().listen({ + channel, + key: { + type: channelKey + }, + handle: (msg) => { + if (msgCmp) { + expect(msg).toEqual(msgCmp); + } + cb(msg); + } + }); + } + + function makeMockNotebook(commInfoReturn, registerTargetReturn, executeReply) { + commInfoReturn = commInfoReturn || DEFAULT_COMM_INFO; + registerTargetReturn = registerTargetReturn || DEFAULT_COMM; + executeReply = executeReply || {} + return { + save_checkpoint: () => { /* no op */ }, + kernel: { + comm_info: (name, cb) => cb(commInfoReturn), + execute: (code, cb) => cb.shell.reply({content: executeReply}), + comm_manager: { + register_comm: () => {}, + register_target: (name, cb) => cb(registerTargetReturn, {}) + } + } + }; + } + + function makeCommMsg(msgType, content) { + return { + content: { + data: { + msg_type: msgType, + content: content + } + } + }; + } + + describe('Test the jobCommChannel widget', () => { + let runtime; + + beforeEach(() => { + runtime = Runtime.make(); + Jupyter.notebook = makeMockNotebook(); + }); + + afterEach(() => { + window.kbaseRuntime = null; + }); + + it('Should load properly', () => { + expect(JobCommChannel).not.toBeNull(); + }); + + it('Should be instantiable and contain the right components', () => { + let comm = new JobCommChannel(); + expect(comm.initCommChannel).toBeDefined(); + expect(comm.jobStates).toEqual({}); + }); + + it('Should initialize correctly in the base case', (done, fail) => { + let comm = new JobCommChannel(); + comm.initCommChannel() + .then(done) + .catch((err) => { + console.error(err); + fail(); + }); + }); + + it('Should re-initialize with an existing channel', (done, fail) => { + let comm = new JobCommChannel(); + Jupyter.notebook = makeMockNotebook({ + content: { + comms: { + '12345': { + target_name: 'KBaseJobs' + } + } + } + }); + comm.initCommChannel() + .then(() => { + expect(comm.comm).not.toBeNull(); + done(); + }) + .catch((err) => { + console.error(err); + fail(); + }); + }); + + it('Should fail to initialize with a failed reply from the JobManager startup', (done, fail) => { + let comm = new JobCommChannel(); + Jupyter.notebook = makeMockNotebook(null, null, { + name: 'Failed to start', + evalue: 'Some error', + error: 'Yes. Very yes.' + }); + comm.initCommChannel() + .then(() => { + console.error('Should not have succeeded.'); + fail(); + }) + .catch((err) => { + expect(err).toEqual(new Error('Failed to start:Some error')); + done(); + }); + }); + + let busMsgCases = [ + ['ping-comm-channel', {pingId: 'ping!'}], + ['request-job-cancellation', {jobId: 'someJob'}], + ['request-job-status', {jobId: 'someJob', parentJobId: 'someParent'}], + ['request-job-update', {jobId: 'someJob', parentJobId: 'someParent'}], + ['request-job-completion', {jobId: 'someJob'}], + ['request-job-log', {jobId: 'someJob', options: {}}], + ['request-latest-job-log', {jobId: 'someJob', options: {}}], + ['request-job-info', {jobId: 'someJob', parentJobId: 'someParent'}] + ]; + busMsgCases.forEach(function (testCase) { + it('Should handle ' + testCase[0] + ' bus message', (done) => { + let comm = new JobCommChannel(); + comm.initCommChannel() + .then(() => { + expect(comm.comm).not.toBeNull(); + spyOn(comm.comm, 'send'); + runtime.bus().emit(testCase[0], testCase[1]); + return new Promise(resolve => setTimeout(resolve, 100)); + }) + .then(() => { + expect(comm.comm.send).toHaveBeenCalled(); + done(); + }); + }); + }); + + it('Should error properly when trying to send a comm with an uninited channel', (done) => { + let comm = new JobCommChannel(); + let prom = comm.sendCommMessage('some_msg', 'foo', {}); + prom.then(() => { + fail('This should have failed'); + }).catch((err) => { + expect(err.message).toContain('ERROR sending comm message'); + done(); + }); + }); + + /* Mocking out comm messages coming back over the channel is gruesome. Just + * calling the handleCommMessage function directly. + */ + it('Should handle a start message', (done) => { + let comm = new JobCommChannel(); + comm.initCommChannel() + .then(() => { + comm.handleCommMessages(makeCommMsg('start', {})); + done(); + }); + }); + + it('Should respond to new_job by saving the Narrative', (done) => { + let comm = new JobCommChannel(); + spyOn(Jupyter.notebook, 'save_checkpoint'); + comm.initCommChannel() + .then(() => { + comm.handleCommMessages(makeCommMsg('new_job', {})); + expect(Jupyter.notebook.save_checkpoint).toHaveBeenCalled(); + done(); + }); + }); + + it('Should send job_status messages to the bus', (done) => { + const jobId = 'someJob', + msg = makeCommMsg('job_status', { + state: { + job_id: jobId + }, + spec: {}, + widget_info: {} + }), + busMsg = { + jobId: jobId, + jobState: { + job_id: jobId + }, + outputWidgetInfo: {} + }; + + let comm = new JobCommChannel(); + + channelChecker('jobId', jobId, 'job-status', done, busMsg); + + comm.initCommChannel() + .then(() => { + comm.handleCommMessages(msg); + }); + }); + + it('Should send a set of job statuses to the bus, and delete extras', (done) => { + let caughtMsgs = 0, + msg = makeCommMsg('job_status_all', { + 'id1': { + state: { + job_id: 'id1' + } + }, + 'id2': { + state: { + job_id: 'id2' + } + } + }); + let comm = new JobCommChannel(); + comm.jobStates['deletedJob'] = {state: {job_id: 'deletedJob'}}; + const msgCounter = () => { + caughtMsgs++; + if (caughtMsgs === 3) { + done(); + } + } + + ['id1', 'id2'].forEach((jobId) => { + channelChecker('jobId', jobId, 'job-status', msgCounter, { + jobId: jobId, + jobState: { + job_id: jobId + } + }); + + }); + channelChecker('jobId', 'deletedJob', 'job-deleted', msgCounter, { + jobId: 'deletedJob', + via: 'no_longer_exists' + }); + comm.initCommChannel() + .then(() => { + comm.handleCommMessages(msg); + }); + }); + + it('Should send a job-info message to the bus', (done) => { + let jobId = 'foo', + msg = makeCommMsg('job_info', { + job_id: jobId, + state: { + job_id: jobId + } + }); + + channelChecker('jobId', jobId, 'job-info', done, { + jobId: jobId, + jobInfo: msg.content.data.content + }); + let comm = new JobCommChannel(); + comm.initCommChannel() + .then(() => { + comm.handleCommMessages(msg); + }); + }); + + it('Should send a run_status message to the bus', (done) => { + let jobId = 'foo', + cellId = 'bar', + msg = makeCommMsg('run_status', { + cell_id: cellId, + job_id: jobId, + }); + channelChecker('cell', cellId, 'run-status', done, msg.content.data.content); + let comm = new JobCommChannel(); + comm.initCommChannel() + .then(() => { + comm.handleCommMessages(msg); + }); + }); + + it('Should send job_canceled message to the bus', (done) => { + let jobId = 'foo-canceled', + msg = makeCommMsg('job_canceled', { + job_id: jobId, + }); + channelChecker('jobId', jobId, 'job-canceled', done, { + jobId: jobId, via: 'job_canceled' + }); + let comm = new JobCommChannel(); + comm.initCommChannel() + .then(() => { + comm.handleCommMessages(msg); + }); + }); + + it('Should send job_does_not_exist messages to the bus', (done) => { + let jobId = 'foo-dne', + msg = makeCommMsg('job_does_not_exist', { + job_id: jobId, + source: 'someSource' + }), + comm = new JobCommChannel(); + channelChecker('jobId', jobId, 'job-does-not-exist', done, { + jobId: jobId, source: 'someSource' + }); + comm.initCommChannel() + .then(() => { + comm.handleCommMessages(msg); + }); + }); + + it('Should send job_logs to the bus', (done) => { + let jobId = 'foo-logs', + msg = makeCommMsg('job_logs', { + job_id: jobId, + latest: true, + logs: [{}] + }), + comm = new JobCommChannel(); + channelChecker('jobId', jobId, 'job-logs', done, { + jobId: jobId, + logs: msg.content.data.content, + latest: msg.content.data.content.latest + }); + comm.initCommChannel() + .then(() => { + comm.handleCommMessages(msg); + }); + }); + + const errCases = { + 'cancel_job': 'job-cancel-error', + 'job_logs': 'job-log-deleted', + 'job_logs_latest': 'job-log-deleted', + 'job_status': 'job-status-error', + }; + + Object.keys(errCases).forEach((errCase) => { + it('Should handle job_comm_error of type ' + errCase, (done) => { + let jobId = 'job-' + errCase, + errMsg = errCase + ' error happened!', + msg = makeCommMsg('job_comm_error', { + source: errCase, + job_id: jobId, + message: errMsg + }), + comm = new JobCommChannel(); + channelChecker('jobId', jobId, errCases[errCase], done, { + jobId: jobId, + message: errMsg + }); + comm.initCommChannel() + .then(() => { + comm.handleCommMessages(msg) + }); + }); + }); + + it('Handle unknown job errors generically', (done) => { + let jobId = 'jobWithErrors', + errMsg = 'some random error', + requestType = 'some-error', + msg = makeCommMsg('job_comm_error', { + source: requestType, + job_id: jobId, + message: errMsg + }), + comm = new JobCommChannel(); + channelChecker('jobId', jobId, 'job-error', done, { + jobId: jobId, + message: errMsg, + request: requestType + }); + comm.initCommChannel() + .then(() => { + comm.handleCommMessages(msg); + }); + }); + + it('Should handle job_init_err and job_init_lookup_err', (done) => { + let count = 0; + ['job_init_err', 'job_init_lookup_err'].forEach((errType) => { + let msg = makeCommMsg(errType, { + service: 'job service', + error: 'An error happened!', + name: 'Error', + source: 'jobmanager' + }), + comm = new JobCommChannel(); + comm.initCommChannel() + .then(() => { + comm.handleCommMessages(msg); + return new Promise((resolve) => { setInterval(resolve, 1000) }); + }) + .then(() => { + expect(document.querySelector('#kb-job-err-report')).not.toBeNull(); + count++; + if (count == 2) { + done(); + } + }); + }); + }); + + it('Should send a result message to the bus', (done) => { + let cellId = 'someCellId', + msg = makeCommMsg('result', { + address: { + cell_id: cellId + }, + result: [1] + }), + comm = new JobCommChannel(); + channelChecker('cell', cellId, 'result', done, msg.content.data.content); + comm.initCommChannel() + .then(() => { + comm.handleCommMessages(msg); + }); + }); + + it('Should handle unknown messages with console warnings', (done) => { + let comm = new JobCommChannel(), + msg = makeCommMsg('unknown_weird_msg', {}) + comm.initCommChannel() + .then(() => { + spyOn(console, 'warn'); + comm.handleCommMessages(msg); + expect(console.warn).toHaveBeenCalled(); + done(); + }); + }); + }); +}); diff --git a/test/unit/spec/narrative_core/kbaseNarrativeAppPanel-spec.js b/test/unit/spec/narrative_core/kbaseNarrativeAppPanel-spec.js index 0e8398c514..fae4e30a9d 100644 --- a/test/unit/spec/narrative_core/kbaseNarrativeAppPanel-spec.js +++ b/test/unit/spec/narrative_core/kbaseNarrativeAppPanel-spec.js @@ -5,10 +5,12 @@ /*jslint white: true*/ define([ 'jquery', + 'jquery-ui', 'kbaseNarrativeAppPanel', 'base/js/namespace', - 'kbaseNarrative' -], function($, AppPanel, Jupyter, Narrative) { + 'kbaseNarrative', + 'bootstrap' +], function($, jqueryui, AppPanel, Jupyter, Narrative) { 'use strict'; var $panel = $('
'); var appPanel = null; @@ -17,6 +19,9 @@ define([ beforeEach(function(done) { Jupyter.narrative = new Narrative(); Jupyter.narrative.userId = 'narrativetest'; + Jupyter.narrative.narrController = { + uiModeIs: p => false + }; // just a dummy mock so we don't see error messages. Don't actually need a kernel. Jupyter.notebook = { kernel: { @@ -29,6 +34,7 @@ define([ }); }); afterEach(function() { + appPanel.detach(); $panel = $('
'); appPanel = null; }); @@ -60,6 +66,25 @@ define([ expect(appPanel.$methodList.children().children().length).toBe(0); }); + it('Should reset search by jquery event removeFilterMethods.Narrative', () => { + expect(appPanel.$methodList.children().children().length).not.toBe(0); + + $(document).trigger('filterMethods.Narrative', 'should show nothing'); + expect(appPanel.$methodList.children().children().length).toBe(0); + $(document).trigger('removeFilterMethods.Narrative'); + expect(appPanel.$methodList.children().children().length).not.toBe(0); + }); + + it('Should toggle search bar visibility by hitting a button', () => { + expect(appPanel.$searchDiv.is(':visible')).toBeFalsy(); + // use the app_offset flag as a proxy for having clicked. + expect(appPanel.app_offset).toBeTrue(); + $panel.find('button.btn .fa-search').parent().click(); + expect(appPanel.app_offset).toBeFalse(); + $panel.find('button.btn .fa-search').parent().click(); + expect(appPanel.app_offset).toBeTrue(); + }); + it('Should have a working filter menu', function() { // Should have a filter menu. var dropdownSelector = '#kb-app-panel-filter'; @@ -159,12 +184,95 @@ define([ expect(appPanel.refreshFromService).toHaveBeenCalledWith('dev'); }); - // it('Should have a working catalog slideout button', function() { + it('Should respond to getFunctionSpecs.Narrative by returning a set of app specs', (done) => { + const cb = (specs) => { + expect(specs).toBeDefined(); + expect(specs.methods['kb_quast/run_QUAST_app']).toEqual(jasmine.any(Object)); + done(); + }; + const appRequest = { + methods: ['kb_quast/run_QUAST_app'] + }; + $(document).trigger('getFunctionSpecs.Narrative', [appRequest, cb]); + }); + + it('Should return empty results when a spec cannot be found', (done) => { + const cb = (specs) => { + expect(specs).toBeDefined(); + expect(specs.methods).toEqual({}); + done(); + }; + const appRequest = { + methods: ['notAModule/notAnApp'] + }; + $(document).trigger('getFunctionSpecs.Narrative', [appRequest, cb]); + }); + + it('Should return an empty list when requesting empty methods', (done) => { + const cb = (specs) => { + console.log(specs); + expect(specs).toEqual({}); + done(); + }; + $(document).trigger('getFunctionSpecs.Narrative', [{}, cb]); + }); + + it('Should have a working catalog slideout button', function() { + // spoof being logged in from the catalog widget's point of view - // }); + $(document).on('loggedInQuery.kbase', (e, cb) => { + if (cb) { + cb({ + token: 'fake_token', + user_id: 'narrativetest', + kbase_sessionid: 'narrativetest_session' + }); + } + }); - // it('Should trigger the insert app function when clicking on an app', function() { + expect(appPanel.appCatalog).toBeNull(); + $panel.find('button.btn .fa-arrow-right').click(); + expect(appPanel.appCatalog).not.toBeNull(); - // }); + $(document).off('loggedInQuery.kbase'); + }); + + it('Should collapse/restore on read only toggling', () => { + spyOn(appPanel, 'toggleCollapse'); + appPanel.setReadOnlyMode(true); + expect(appPanel.toggleCollapse).toHaveBeenCalledWith('collapse'); + appPanel.setReadOnlyMode(false); + expect(appPanel.toggleCollapse).toHaveBeenCalledWith('restore'); + }); + + it('Should know how to set its list height', (done) => { + // appPanel.setListHeight('10px', true); + // expect(appPanel.$methodList.css('height')).toEqual('10px'); + appPanel.setListHeight('100px', false); + expect(appPanel.$methodList.css('height')).toEqual('100px'); + appPanel.setListHeight('123px', true); + setTimeout(() => { + expect(appPanel.$methodList.css('height')).toEqual('123px'); + done(); + }, 1000); + }); + + xit('Should trigger the insert app function when clicking on an app', (done) => { + $(document).on('appClicked.Narrative', (app, tag, params) => { + console.log(app); + console.log(tag); + console.log(params); + done(); + }); + appPanel.triggerApp('megahit/run_megahit', 'release'); + }); + + it('Should know how to show an error', () => { + const title = 'Error Title', + error = 'Error string'; + appPanel.showError(title, error); + expect(appPanel.$errorPanel.html()).toContain(title); + expect(appPanel.$errorPanel.html()).toContain(error); + }); }); }); diff --git a/test/unit/spec/narrative_core/kbaseNarrativeJobsPanel-spec.js b/test/unit/spec/narrative_core/kbaseNarrativeJobsPanel-spec.js deleted file mode 100644 index 9c8678732e..0000000000 --- a/test/unit/spec/narrative_core/kbaseNarrativeJobsPanel-spec.js +++ /dev/null @@ -1,14 +0,0 @@ -/*global define*/ -/*global describe, it, expect*/ -/*global jasmine*/ -/*global beforeEach, afterEach*/ -/*jslint white: true*/ -define([ - 'kbaseNarrativeJobsPanel' -], function(Widget) { - describe('Test the kbaseNarrativeJobsPanel widget', function() { - it('Should do things', function() { - - }); - }); -});
').append(key)).append($('').append(value)); + } + + $overviewTable.append(get_table_row('KBase Object Name', + '' + self.ss_obj_info[1] +'' )); + // leave out version for now, because that is not passed into data widgets + //'' + self.ss_obj_info[1] + ' (v'+self.ss_obj_info[4]+')'+'' )); + $overviewTable.append(get_table_row('Saved by', String(self.ss_obj_info[5]))); + $overviewTable.append(get_table_row('Number of Samples', self.ss_obj_data['samples'].length )); + $overviewTable.append(get_table_row('Description', self.ss_obj_data['description'])); + + self.metadata_headers = [{ + id: "sample_version", + text: "version", + isSortable: true + }]; // version not in metadata, but included in visualization. + // get the metadata_keys + self.client.sync_call('SampleService.get_sample', [{ + id: self.ss_obj_data['samples'][0]['id'] + }]).then(function(sample){ + if (sample.length > 0 && 'node_tree' in sample[0] && sample[0]['node_tree'].length > 0){ + var node_tree = sample[0]['node_tree'][0] + Object.keys(node_tree['meta_controlled']).concat(Object.keys(node_tree['meta_user'])).forEach( function(metakey){ + self.metadata_headers.push({ + id: metakey.split(" ").join('_'), + text: metakey, + isSortable: true + }) + }) + } else { + console.error('Error: Could not load the first sample for metadata headers: ' + err); + } + }) + // Build the tabs + var $tabs = new kbaseTabs($tabPane, { + tabPosition : 'top', + canDelete : false, //whether or not the tab can be removed. + tabs : [ + { + tab : 'Summary', //name of the tab + content : $('
').css('margin-top','15px').append($overviewTable), + show : true, + }, { + tab : 'Samples', + showContentCallback: function() { + return self.addSamplesList(); + } + }, + ], + }); + }, + + + addSamplesList: function() { + var self = this; + var $content = $('
'); + new DynamicTable($content, { + headers: [{ + id: 'name', + text: 'Sample Name', + isSortable: true + }, { + id: 'sample_id', + text: 'Sample ID', + isSortable: false + }].concat(self.metadata_headers), + searchPlaceholder: 'Search samples', + updateFunction: function(pageNum, query, sortColId, sortColDir) { + var rows = []; + var sample_slice = self.ss_obj_data['samples'].slice( + pageNum * self.options.pageLimit, (pageNum + 1) * self.options.pageLimit + ); + var sample_queries = []; + var sample_data = []; + sample_slice.forEach(function (sample_info) { + var sample_query_params = { + id: sample_info['id'] + } + if ("version" in sample_info){ + sample_query_params['version'] = sample_info['version'] + } + sample_queries.push( + Promise.resolve(self.client.sync_call('SampleService.get_sample', [sample_query_params])).then(function(sample){ + let samp_data = {}; + samp_data['version'] = String(sample[0]['version']) + for (let i = 0; i < sample[0]['node_tree'].length; i++){ + for (const meta_key in sample[0]['node_tree'][i]['meta_controlled']){ + samp_data[meta_key] = self.unpack_metadata_to_string( + sample[0]['node_tree'][i]['meta_controlled'][meta_key] + ); + } + for (const meta_key in sample[0]['node_tree'][i]['meta_user']){ + samp_data[meta_key] = self.unpack_metadata_to_string( + sample[0]['node_tree'][i]['meta_user'][meta_key] + ); + } + } + sample_data.push(samp_data) + }) + ) + var row = [sample_info['name'], sample_info['id']]; + rows.push(row); + }); + return Promise.all(sample_queries).then(function(){ + for (let j = 0; j < rows.length; j++){ + var row = rows[j]; + for (const meta_idx in self.metadata_headers){ + var meta_header = self.metadata_headers[meta_idx] + var row_str = self.options.default_blank_value; + if (meta_header.text in sample_data[j]){ + row_str = sample_data[j][meta_header.text] + } + row.push(row_str); + } + rows[j] = row + } + return { + rows: rows, + start: pageNum * self.options.pageLimit, + query: query, + total: self.ss_obj_data['samples'].length + } + }) + }, + style: {'margin-top': '5px'} + }); + return $content; + }, + + unpack_metadata_to_string: function(metadata){ + var metastr = String(metadata['value']); + if ('units' in metadata){ + metastr += " " + String(metadata['units']); + } + return metastr + }, + + }); + +}); diff --git a/kbase-extension/static/kbase/js/widgets/kbaseSessionSync.js b/kbase-extension/static/kbase/js/widgets/kbaseSessionSync.js deleted file mode 100644 index 3c2b125bc3..0000000000 --- a/kbase-extension/static/kbase/js/widgets/kbaseSessionSync.js +++ /dev/null @@ -1,143 +0,0 @@ -(function($) { -'use strict'; - var SessionSync = Object.create({}, { - init: { - value: function (cfg) { - this.sessionObject = this.importSessionFromCookie(); - return this; - } - }, - - session: { - value: null, - writable: true - }, - sessionObject: { - value: null, - writable: true - }, - cookieName: { - value: 'kbase_session' - }, - - // Note that THIS session just uses the original kbase - // session object without transforming it to the canonical form - // used in the real kbaseSession - getKBaseSession: { - value: function () { - return this.sessionObject; - } - }, - - - importSessionFromCookie: { - value: function () { - var sessionCookie = $.cookie(this.cookieName); - if (!sessionCookie) { - return null; - } - // first pass just break out the string into fields. - var session = this.decodeToken(sessionCookie); - - if (! (session.kbase_sessionid && session.un && session.user_id && session.token) ) { - this.removeAuth(); - return null; - } - - session.token = session.token.replace(/PIPESIGN/g, '|').replace(/EQUALSSIGN/g, '='); - - // now we have a session object equivalent to the one returned by the auth service. - - session.tokenObject = this.decodeToken(session.token); - - if (this.validateSession(session)) { - return session; - } else { - return null; - } - } - }, - decodeToken: { - value: function (token) { - var parts = token.split('|'); - var map = {}; - for (var i=0; i { + this.sendCommMessage('ping', null, { + ping_id: message.pingId + }); + }); + + // Cancels the job. + bus.on('request-job-cancellation', (message) => { + this.sendCommMessage(CANCEL_JOB, message.jobId); + }); + + // Fetches job status from kernel. + bus.on('request-job-status', (message) => { + // console.log('requesting job status for ' + message.jobId); + this.sendCommMessage(JOB_STATUS, message.jobId, { parent_job_id: message.parentJobId }); + }); + + // Requests job status updates for this job via the job channel, and also + // ensures that job polling is running. + bus.on('request-job-update', (message) => { + // console.log('requesting job updates for ' + message.jobId); + this.sendCommMessage(START_JOB_UPDATE, message.jobId, { parent_job_id: message.parentJobId }); + }); + + // Tells kernel to stop including a job in the lookup loop. + bus.on('request-job-completion', (message) => { + // console.log('cancelling job updates for ' + message.jobId); + this.sendCommMessage(STOP_JOB_UPDATE, message.jobId, { parent_job_id: message.parentJobId }); + }); + + // Fetches job logs from kernel. + bus.on('request-job-log', (message) => { + this.sendCommMessage(JOB_LOGS, message.jobId, message.options); + }); + + // Fetches most recent job logs from kernel. + bus.on('request-latest-job-log', (message) => { + this.sendCommMessage(JOB_LOGS_LATEST, message.jobId, message.options); + }); + + // Fetches info (not state) about a job. Like the app id, name, and inputs. + bus.on('request-job-info', (message) => { + this.sendCommMessage(JOB_INFO, message.jobId, { parent_job_id: message.parentJobId }); + }); + } + + /** + * Sends a comm message to the JobManager in the kernel. + * If there's no comm channel ready, tries to set one up first. + * @param msgType {string} - one of (prepend with this.) + * ALL_STATUS, + * STOP_UPDATE_LOOP, + * START_UPDATE_LOOP, + * STOP_JOB_UPDATE, + * START_JOB_UPDATE, + * JOB_LOGS + * @param jobId {string} - optional - a job id to send along with the + * message, where appropriate. + */ + sendCommMessage(msgType, jobId, options) { + return new Promise((resolve, reject) => { + // TODO: send specific error so that client can retry. + if (!this.comm) { + console.error('Comm channel not initialized, not sending message.'); + reject(new Error('Comm channel not initialized, not sending message.')); + } + + var msg = { + target_name: COMM_NAME, + request_type: msgType + }; + if (jobId) { + msg.job_id = jobId; + } + if (options) { + msg = $.extend({}, msg, options); + } + this.comm.send(msg); + resolve(); + }) + .catch((err) => { + console.error('ERROR sending comm message', err, msgType, jobId, options); + throw new Error('ERROR sending comm message', err, msgType, jobId, options); + }); + } + + /** + * Callback attached to the comm channel. This gets called with the message when + * a message is passed. + * The message is expected to have the following structure (at a minimum): + * { + * content: { + * data: { + * msg_type: string, + * content: object + * } + * } + * } + * Where msg_type is one of: + * start, new_job, job_status, job_status_all, job_info, run_status, job_err, job_canceled, + * job_does_not_exist, job_logs, job_comm_err, job_init_err, job_init_partial_err, + * job_init_lookup_err, result. + * @param {object} msg + */ + handleCommMessages(msg) { + var msgType = msg.content.data.msg_type; + var msgData = msg.content.data.content; + var jobId = null; + switch (msgType) { + case 'start': + // console.log('START', msgData.time); + break; + case 'new_job': + Jupyter.notebook.save_checkpoint(); + break; + /* + * The job status for one or more jobs. See job_status_all + * for a message which covers all active jobs. + * Note that these messages are additive to the job panel + * cache, but the reverse logic does not apply. + */ + case 'job_status': + jobId = msgData.state.job_id; + // We could just copy the entire message into the job + // states cache, but referencing each individual property + // is more explicit about the structure. + this.jobStates[msgData.state.job_id] = { + state: msgData.state, + spec: msgData.spec, + widgetParameters: msgData.widget_info + }; + + /* + * Notify the front end about the changed or new job + * states. + */ + this.sendBusMessage(JOB, jobId, 'job-status', { + jobId: jobId, + jobState: msgData.state, + outputWidgetInfo: msgData.widget_info + }); + break; + /* + * This message must carry all jobs linked to this narrative. + * The "job-deleted" logic, specifically, requires that the job + * actually not exist in the job service. + * NB there is logic in the job management back end to allow + * job notification to be turned off per job -- this would + * be incompatible with the logic here and we should address + * that. + * E.g. if that behavior is allowed, then deletion detection + * would need to move to the back end, since that is the only + * place that would truly know about all jobs for this narrative. + */ + case 'job_status_all': + /* + * Ensure there is a locally cached copy of each job. + * + */ + for (jobId in msgData) { + const jobStateMessage = msgData[jobId]; + // We could just copy the entire message into the job + // states cache, but referencing each individual property + // is more explicit about the structure. + this.jobStates[jobId] = { + state: jobStateMessage.state, + spec: jobStateMessage.spec, + widgetParameters: jobStateMessage.widget_info, + owner: jobStateMessage.owner + }; + + this.sendBusMessage(JOB, jobId, 'job-status', { + jobId: jobId, + jobState: jobStateMessage.state, + outputWidgetInfo: jobStateMessage.widget_info + }); + } + + Object.keys(this.jobStates).forEach((jobId) => { + if (!msgData[jobId]) { + // If this job is not found in the incoming list of all + // jobs, then we must both delete it locally, and + // notify any interested parties. + this.sendBusMessage(JOB, jobId, 'job-deleted', { + jobId: jobId, + via: 'no_longer_exists' + }); + // it is safe to delete properties here + delete this.jobStates[jobId]; + } + }); + break; + case 'job_info': + jobId = msgData.job_id; + this.sendBusMessage(JOB, jobId, 'job-info', { + jobId: jobId, + jobInfo: msgData + }); + break; + case 'run_status': + // Send job status notifications on the default channel, + // with a key on the message type and the job id, sending + // a copy of the original message. + // This allows widgets which are interested in the job + // to subscribe to just that job, and nothing else. + // If there is a need for a generic broadcast message, we + // can either send a second message or implement key + // filtering. + this.sendBusMessage(CELL, msgData.cell_id, 'run-status', msgData); + break; + case 'job_canceled': + var canceledId = msgData.job_id; + this.sendBusMessage(JOB, canceledId, 'job-canceled', + { jobId: canceledId, via: 'job_canceled' }); + break; + + case 'job_does_not_exist': + this.sendBusMessage(JOB, msgData.job_id, 'job-does-not-exist', + { jobId: msgData.job_id, source: msgData.source }); + break; + + case 'job_logs': + jobId = msgData.job_id; + this.sendBusMessage(JOB, jobId, 'job-logs', { + jobId: jobId, + logs: msgData, + latest: msgData.latest + }); + break; + + case 'job_comm_error': + if (msgData) { + jobId = msgData.job_id; + switch (msgData.source) { + case 'cancel_job': + this.sendBusMessage(JOB, jobId, 'job-cancel-error', { + jobId: jobId, + message: msgData.message + }); + break; + case 'job_logs': + case 'job_logs_latest': + this.sendBusMessage(JOB, jobId, 'job-log-deleted', { + jobId: jobId, + message: msgData.message + }); + break; + case 'job_status': + this.sendBusMessage(JOB, jobId, 'job-status-error', { + jobId: jobId, + message: msgData.message + }); + break; + default: + this.sendBusMessage(JOB, jobId, 'job-error', { + jobId: jobId, + message: msgData.message, + request: msgData.source + }); + break; + } + } + console.error('Error from job comm:', msg); + break; + + case 'job_init_err': + case 'job_init_lookup_err': + /* + code, error, job_id (opt), message, name, source + */ + var $modalBody = $(Handlebars.compile(JobInitErrorTemplate)(msgData)); + var modal = new BootstrapDialog({ + title: 'Job Initialization Error', + body: $modalBody, + buttons: [ + $('') + .append('OK') + .click(function (event) { + modal.hide(); + }) + ] + }); + new kbaseAccordion($modalBody.find('div#kb-job-err-trace'), { + elements: [{ + title: 'Detailed Error Information', + body: $('' + + '' + + (function () { + if (msgData.service) { + return ''; + } + return ''; + }()) + + '' + + '
code:' + msgData.code + '
error:' + msgData.message + '
service:' + msgData.service + '
type:' + msgData.name + '
source:' + msgData.source + '
') + }] + }); + + $modalBody.find('button#kb-job-err-report').click(function (e) { + + }); + modal.getElement().on('hidden.bs.modal', function () { + modal.destroy(); + }); + modal.show(); + break; + case 'result': + this.sendBusMessage(CELL, msgData.address.cell_id, 'result', msgData); + break; + default: + console.warn('Unhandled KBaseJobs message from kernel (type=\'' + msgType + '\'):'); + console.warn(msg); + } + } + + /** + * Initializes the comm channel to the back end. Stores the generated + * channel in this.comm. + * Returns a Promise that should resolve when the channel's ready. But + * the nature of setting these up means that a nested Promise gets made by + * the Jupyter kernel front-end and not necessarily returned. + * + * Thus, it uses a semaphore lock. When the semaphore becomes ready, it + * gets signaled. + * + * Once ready, this starts a kernel call (over the main channel, not the + * new comm) to initialize the JobManager and have it fetch the set of + * running jobs from the execution engine. If it's already running, this + * overwrites everything. + */ + initCommChannel() { + const _this = this; + _this.comm = null; + let commSemaphore = Semaphore.make(); + commSemaphore.add('comm', false); + return new Promise((resolve, reject) => { + // First we check to see if our comm channel already + // exists. If so, we do some funny business to create a + // new client side for it, register it, and set up our + // handler on it. + Jupyter.notebook.kernel.comm_info(COMM_NAME, (msg) => { + if (msg.content && msg.content.comms) { + // skim the reply for the right id + for (const id in msg.content.comms) { + if (msg.content.comms[id].target_name === COMM_NAME) { + _this.comm = new JupyterComm.Comm(COMM_NAME, id); + Jupyter.notebook.kernel.comm_manager.register_comm(_this.comm); + _this.comm.on_msg(_this.handleCommMessages.bind(_this)); + } + } + } + resolve(); + }); + }) + .then(() => { + // If no existing comm channel could be hooked up to, we have an alternative + // strategy, apparently. We register our channel endpoint, even though there is + // no back end yet, and our next call to utilize it below will create it. + if (_this.comm) { + commSemaphore.set('comm', 'ready'); + return; + } + return Promise.try(() => { + Jupyter.notebook.kernel.comm_manager.register_target(COMM_NAME, (comm, msg) => { + _this.comm = comm; + comm.on_msg(_this.handleCommMessages.bind(_this)); + commSemaphore.set('comm', 'ready'); + }); + }); + }) + .then(() => { + return new Promise((resolve, reject) => { + const callbacks = { + shell: { + reply: function (reply) { + if (reply.content.error) { + console.error('ERROR executing jobInit', reply); + commSemaphore.set('comm', 'error'); + reject(new Error(reply.content.name + ':' + reply.content.evalue)); + } else { + resolve(); + } + } + } + }; + Jupyter.notebook.kernel.execute(_this.getJobInitCode(), callbacks); + }); + }); + } + + getJobInitCode() { + return [ + 'from biokbase.narrative.jobs.jobcomm import JobComm', + 'JobComm().start_job_status_loop(init_jobs=True)' + ].join('\n'); + } + } + + return JobCommChannel; +}); diff --git a/kbase-extension/static/kbase/js/widgets/narrative_core/kbaseNarrativeAppPanel.js b/kbase-extension/static/kbase/js/widgets/narrative_core/kbaseNarrativeAppPanel.js index 1c820a7520..521cdf3812 100644 --- a/kbase-extension/static/kbase/js/widgets/narrative_core/kbaseNarrativeAppPanel.js +++ b/kbase-extension/static/kbase/js/widgets/narrative_core/kbaseNarrativeAppPanel.js @@ -18,7 +18,6 @@ define([ 'util/display', 'util/bootstrapDialog', 'util/bootstrapSearch', - 'util/icon', 'text!kbase/templates/beta_warning_body.html', 'yaml!ext_components/kbase-ui-plugin-catalog/src/plugin/modules/data/categories.yml', 'kbaseAccordion', @@ -43,7 +42,6 @@ define([ DisplayUtil, BootstrapDialog, BootstrapSearch, - Icon, BetaWarningTemplate, Categories, kbaseAccordion, @@ -95,24 +93,8 @@ define([ this.getIgnoreCategories(); - this.icon_colors = Config.get('icons').colors; - this.$searchDiv = $('
').hide(); - this.$numHiddenSpan = $('0'); - this.$showHideSpan = $('show'); - this.$toggleHiddenDiv = $('
') - .append(this.$showHideSpan) - .append(' ') - .append(this.$numHiddenSpan) - .append(' filtered out') - .addClass('kb-function-toggle') - .hide() - .click($.proxy(function() { - var curText = this.$showHideSpan.text(); - this.toggleHiddenMethods(curText === 'show'); - this.$showHideSpan.text(curText === 'show' ? 'hide' : 'show'); - }, this)); // placeholder for apps and methods once they're loaded. this.$methodList = $('
') @@ -126,8 +108,7 @@ define([ this.$functionPanel = $('
') .addClass('kb-function-body') .append($('
') - .append(this.$searchDiv) - .append(this.$toggleHiddenDiv)) + .append(this.$searchDiv)) .append(this.$methodList); this.bsSearch = new BootstrapSearch(this.$searchDiv, { @@ -195,9 +176,7 @@ define([ */ $(document).on('getFunctionSpecs.Narrative', $.proxy(function(e, specSet, callback) { - //console.debug("Trigger proxy: specSet=", specSet, "callback=", callback); if (callback) { - //console.debug("Trigger: specSet=",specSet); this.getFunctionSpecs(specSet, callback); } }, this) @@ -394,19 +373,19 @@ define([ this.addButton(this.$slideoutBtn); - if (!NarrativeMethodStore || !Catalog) { - this.showError('Sorry, an error occurred while loading Apps.', - 'Unable to connect to the Catalog or Narrative Method Store! ' + - 'Apps are currently unavailable.'); - return this; - } - this.methClient = new NarrativeMethodStore(this.options.methodStoreURL); this.catalog = new Catalog(this.options.catalogURL, {token: Runtime.make().authToken()}); this.refreshFromService(); return this; }, + detach: function () { + $(document).off('filterMethods.Narrative'); + $(document).off('removeFilterMethods.Narrative'); + $(document).off('getFunctionSpecs.Narrative'); + this.$bodyDiv.detach(); + }, + refreshKernelSpecManager: function() { try { Jupyter.notebook.kernel.execute( @@ -422,9 +401,9 @@ define([ setListHeight: function(height, animate) { if (this.$methodList) { if (animate) { - this.$methodList.animate({ 'height': height}, this.slideTime); // slideTime comes from kbaseNarrativeControlPanel + this.$methodList.animate({'height': height}, this.slideTime); // slideTime comes from kbaseNarrativeControlPanel } else { - this.$methodList.css({ 'height': height}); + this.$methodList.css({'height': height}); } } }, @@ -761,7 +740,7 @@ define([ }); }; - // 1. Go through filterString and keep those that pass the filter (not yet). + // 1. Go through filterString and keep those that pass the filter. appSet = this.filterApps(filterString, appSet); // 2. Switch over panelStyle and build the view based on that. @@ -828,7 +807,8 @@ define([ return [ app.info.name, app.info.input_types.join(';'), - app.info.output_types.join(';') + app.info.output_types.join(';'), + app.info.module_name ].join(';').toLowerCase().indexOf(filterString) !== -1; } var lowerSearchSet = searchSet.map(function(val) { return val.toLowerCase(); }); @@ -984,11 +964,6 @@ define([ this.$functionPanel.hide(); this.$loadingPanel.hide(); this.$errorPanel.show(); - return; }, - - toggleOverlay: function() { - this.trigger('toggleSidePanelOverlay.Narrative'); - } }); }); diff --git a/kbase-extension/static/kbase/js/widgets/narrative_core/kbaseNarrativeDownloadPanel.js b/kbase-extension/static/kbase/js/widgets/narrative_core/kbaseNarrativeDownloadPanel.js index b073a1fc44..5b91487a04 100644 --- a/kbase-extension/static/kbase/js/widgets/narrative_core/kbaseNarrativeDownloadPanel.js +++ b/kbase-extension/static/kbase/js/widgets/narrative_core/kbaseNarrativeDownloadPanel.js @@ -44,7 +44,7 @@ define ([ exportURL: Config.url('data_import_export'), useDynamicDownloadSupport: false, nmsURL: Config.url('narrative_method_store'), - eeURL: Config.url('job_service'), + eeURL: Config.url('execution_engine2'), srvWizURL: Config.url('service_wizard'), timer: null, downloadSpecCache: null, // {'lastUpdateTime': , 'types': {: }} @@ -182,12 +182,14 @@ define ([ var tag = this.getVersionTag(); var method = descr.local_function.replace('/', '.'); var genericClient = new GenericClient(this.eeURL, {token: this.token}, null, false); + Promise.resolve(genericClient.sync_call( - "NarrativeJobService.run_job", + "execution_engine2.run_job", [{ method: method, params: [{input_ref: ref}], service_ver: tag, + app_id: method, }])) .then(data => { var jobId = data[0]; @@ -207,49 +209,51 @@ define ([ var skipLogLines = 0; var lastLogLine = null; var timeLst = function(event) { - genericClient.sync_call("NarrativeJobService.check_job", [jobId], function(data) { + genericClient.sync_call("execution_engine2.check_job", [{"job_id": jobId}], function(data) { var jobState = data[0]; - genericClient.sync_call("NarrativeJobService.get_job_logs", - [{"job_id": jobId, "skip_lines": skipLogLines}], function(data2) { - var logLines = data2[0].lines; - for (var i = 0; i < logLines.length; i++) { - lastLogLine = logLines[i]; - if (lastLogLine.is_error) { - console.error("Export logging: " + lastLogLine.line); - } else { - console.log("Export logging: " + lastLogLine.line); + if (jobState['running']) { + genericClient.sync_call("execution_engine2.get_job_logs", + [{"job_id": jobId, "skip_lines": skipLogLines}], function(data2) { + var logLines = data2[0].lines; + for (var i = 0; i < logLines.length; i++) { + lastLogLine = logLines[i]; + if (lastLogLine.is_error) { + console.error("Export logging: " + lastLogLine.line); + } else { + console.log("Export logging: " + lastLogLine.line); + } } - } - skipLogLines += logLines.length; - var complete = jobState['finished']; - var error = jobState['error']; - if (complete) { - self.stopTimer(); - if (error) { - console.error(error); - self.showError(error['message']); + skipLogLines += logLines.length; + var complete = jobState['finished']; + var error = jobState['error']; + if (complete) { + self.stopTimer(); + if (error) { + console.error(error); + self.showError(error['message']); + } else { + console.log("Export is complete"); + // Starting download from Shock + self.$statusDiv.hide(); + self.$elem.find('.kb-data-list-btn').prop('disabled', false); + var result = jobState['job_output']['result']; + self.downloadUJSResults(result[0].shock_id, self.shockURL, + wsObjectName); + } } else { - console.log("Export is complete"); - // Starting download from Shock - self.$statusDiv.hide(); - self.$elem.find('.kb-data-list-btn').prop('disabled', false); - var result = jobState['result']; - self.downloadUJSResults(result[0].shock_id, self.shockURL, - wsObjectName); + var status = skipLogLines == 0 ? jobState['job_state'] : + lastLogLine.line; + if (skipLogLines == 0) + console.log("Export status: " + status); + self.showMessage(' ' + + 'Export status: ' + status); } - } else { - var status = skipLogLines == 0 ? jobState['job_state'] : - lastLogLine.line; - if (skipLogLines == 0) - console.log("Export status: " + status); - self.showMessage(' ' + - 'Export status: ' + status); - } - }, function(data) { - self.stopTimer(); - console.log(data.error.message); - self.showError(data.error.message); - }); + }, function(data) { + self.stopTimer(); + console.log(data.error.message); + self.showError(data.error.message); + }) + }; }, function(data) { self.stopTimer(); console.log(data.error.message); diff --git a/kbase-extension/static/kbase/js/widgets/narrative_core/kbaseNarrativeJobStatus.js b/kbase-extension/static/kbase/js/widgets/narrative_core/kbaseNarrativeJobStatus.js index 61ad16259f..b8051910f9 100644 --- a/kbase-extension/static/kbase/js/widgets/narrative_core/kbaseNarrativeJobStatus.js +++ b/kbase-extension/static/kbase/js/widgets/narrative_core/kbaseNarrativeJobStatus.js @@ -260,7 +260,7 @@ define([ this.view.statusPanel = this.updateJobStatusPanel(); this.view.body.append($(this.view.statusPanel)); - if (this.state.job_state === 'completed') { + if (this.state.status === 'completed') { // If job's complete, and we have a report, show that. if (this.outputWidgetInfo && this.outputWidgetInfo.params && this.outputWidgetInfo.params.report_ref && !this.showingReport) { @@ -493,9 +493,8 @@ define([ with the buttons (this.userEngaged), play the log when in-progress. 3. userEngaged - if at end of log, same as autoplay? */ - switch (message.jobState.job_state) { - case 'canceled': - case 'suspend': + switch (message.jobState.status) { + case 'terminated': case 'completed': if (this.requestedUpdates) { this.requestedUpdates = false; @@ -513,7 +512,7 @@ define([ this.requestedUpdates = true; this.requestJobStatus(); break; - case 'in-progress': + case 'running': this.requestedUpdates = true; this.requestJobStatus(); break; @@ -568,7 +567,7 @@ define([ var info = { jobId: this.jobId, - status: this.state.job_state === 'suspend' ? 'error' : this.state.job_state, + status: this.state.status === 'suspend' ? 'error' : this.state.status, creationTime: TimeFormat.readableTimestamp(this.state.creation_time), queueTime: elapsedQueueTime, queuePos: this.state.position ? this.state.position : null, diff --git a/kbase-extension/static/kbase/js/widgets/narrative_core/kbaseNarrativeJobsPanel.js b/kbase-extension/static/kbase/js/widgets/narrative_core/kbaseNarrativeJobsPanel.js deleted file mode 100644 index 10e1e96822..0000000000 --- a/kbase-extension/static/kbase/js/widgets/narrative_core/kbaseNarrativeJobsPanel.js +++ /dev/null @@ -1,1073 +0,0 @@ -/*global define*/ -/*jslint white:true,browser:true,maxerr:100*/ -define([ - 'kbwidget', - 'bootstrap', - 'bluebird', - 'jquery', - 'handlebars', - 'narrativeConfig', - 'kbasePrompt', - 'kbaseNarrativeControlPanel', - 'kbaseAccordion', - 'util/bootstrapDialog', - 'util/timeFormat', - 'util/string', - 'base/js/namespace', - 'common/runtime', - 'kb_service/client/workspace', - 'services/kernels/comm', - 'common/semaphore', - 'text!kbase/templates/job_panel/job_info.html', - 'text!kbase/templates/job_panel/job_error.html', - 'text!kbase/templates/job_panel/job_init_error.html' -], function ( - KBWidget, - bootstrap, - Promise, - $, - Handlebars, - Config, - kbasePrompt, - kbaseNarrativeControlPanel, - kbaseAccordion, - BootstrapDialog, - TimeFormat, - StringUtil, - Jupyter, - Runtime, - Workspace, - JupyterComm, - Semaphore, - JobInfoTemplate, - JobErrorTemplate, - JobInitErrorTemplate -) { - 'use strict'; - return KBWidget({ - COMM_NAME: 'KBaseJobs', - ALL_STATUS: 'all_status', - JOB_STATUS: 'job_status', - STOP_UPDATE_LOOP: 'stop_update_loop', - START_UPDATE_LOOP: 'start_update_loop', - STOP_JOB_UPDATE: 'stop_job_update', - START_JOB_UPDATE: 'start_job_update', - DELETE_JOB: 'delete_job', - CANCEL_JOB: 'cancel_job', - JOB_LOGS: 'job_logs', - JOB_LOGS_LATEST: 'job_logs_latest', - JOB_INFO: 'job_info', - - name: 'kbaseNarrativeJobsPanel', - parent: kbaseNarrativeControlPanel, - version: '0.0.1', - options: { - loadingImage: Config.get('loading_gif'), - workspaceUrl: Config.url('workspace'), - autopopulate: true, - title: 'Jobs' - }, - title: $('Jobs '), - // these are the elements that contain running apps and methods - $appsList: null, - $methodsList: null, - // has 'spec' and 'state' keys - populated from server. - jobStates: {}, - comm: null, - init: function (options) { - this._super(options); - this.$jobCountBadge = $('').addClass('label label-danger'); - this.title.append(this.$jobCountBadge); - Handlebars.registerHelper('colorStatus', function (status) { - var s = status.string.toLowerCase(); - switch (s) { - case 'in-progress': - return '' + status + ''; - case 'queued': - return '' + status + ''; - default: - return status; - } - }); - this.jobInfoTmpl = Handlebars.compile(JobInfoTemplate); - this.jobErrorTmpl = Handlebars.compile(JobErrorTemplate); - this.runtime = Runtime.make(); - var $refreshBtn = $('
5d64935ab215ad4128de94d6NarrativeTest/test_editor2019-08-26 ", html) + self.assertIn(":54:48fake_test_usercompletedNot startedIncomplete