-
Notifications
You must be signed in to change notification settings - Fork 53
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
PTV-1905 - add service widget support #3421
base: develop
Are you sure you want to change the base?
Conversation
Hi @briehl , this is the WIP PR for adding service widgets to the Narrative. |
I think using preact is fine, spending time reworking into jquery isn't gonna help that much. I'm still picking through reading this, as there's a lot to review. But we can get it up on CI anyway to keep you unblocked. |
Thanks @briehl! The service widgets team will be elated! I'll catch up the branch with develop later before we deploy it on CI, and get those merge conflicts resolved. Also, I've been meaning to confirm this - it looks like each PR includes Of course, I'm familiar with this practice. That is how old kbase-ui plugins worked, as there was no "build" for them on the github side. Not a big deal, but I know the Narrative has some other files that get changed during development that should be kept out of commits, like the config and narrative version files. |
c'mon peeps, lets start the (p)React train!
or at least try another idea.
…le wrapper [PTV-1905]
@briehl I can check in the changes with the seemingly correct dependencies, to preserve that work. |
and rebuilt package-lock.json [PTV-2905]
@briehl I've asked in the preact slack about getting prop types working with the requirejs/amd scenario. So ... I'm not sure it is worth adding the dependencies. Maybe if I get a useful response we can add it. |
also add a gallery page to demostrate it
aids in iteration with dependencies
@briehl Actually, I just got it working. The docs say that it should issue a console warning on a validation fail, so I was filtering the console on warnings, but then I saw someone say that it is actually issued as console.error, but with the message prefix of "Warning: ..." I would have surely have eventually noticed the errors... E.g. Failed prop type: Invalid prop At least the error message is no longer prefixed with "Warning:" ! |
Now that it is working, I'll go back and add proptypes to all of the preact components. |
@eapearson I'm still somewhat wrestling with dependencies and builds between the narrative-base-image and narrative repos. Hopefully those will get ironed out soon - right now there's some change that's affecting unit tests in an async/hard to repeat way. Then we can see if (unit, at least) tests will pass and try to finalize a review here. |
I'll keep poking at this every couple of days. Got through one batch of prop-types, will do another round next week. |
…es references [PTV-1905]
should circle back and take care of the remaining TODOs.
Had another look at the object spread issue, since it is the source of most of the sonar issues. It ends up that esprima, which has been poorly maintained and has a longstanding bug wrt object spread, is used by two of our dependencies - terser and requirejs's r.js. Terser has a solution, which is to use acorn for parsing. Requirejs, however, has not really been touched in years, so is very unlikely to ever be switched away from esprima, which itself is unlikely to ever be fixed. Interestingly, eslint used to use esprima, but switched away to to espree/acorn. |
@briehl prop-type support has been added to all the preact components. Tests to cover then can be added at some point - they will just have to test that the appropriate warning is issued to the console. For now, I added gallery pages for some of the affected components, so that at least one can poke at the components with props that violate the type specs, and manually inspect the warnings. |
Quality Gate failedFailed conditions See analysis details on SonarCloud Catch issues before they fail your Quality Gate with our IDE extension SonarLint |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A long time coming, but I think - technically - this is great. I still kinda have issues with the overall design of the project and implications it'll have, but the code here in this PR isn't a reflection on that, and is fine. I have a few comments that I've built up over time spent reviewing, but I think they can be addressed in a future PR.
Two new external libraries are added; `preact` and `htm` have been added to support a | ||
more familiar (dare we say "modern"?) style of component architecture. | ||
|
||
> This decision can be reversed, and we can rewrite the components in jquery, but the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No need to rewrite, IMO.
} | ||
renderContent() { | ||
if (this.props.render) { | ||
return this.props.render(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I might be misreading this, but will this cause some strange looping if the render prop is the same as the render function?
What's the render
prop expected to be here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I may have made this comment before you started adding PropTypes
require(['jquery', 'narrativeConfig', 'narrativeLogin'], ($, Config, Login) => { | ||
console.log('Loading KBase Narrative setup routine.'); | ||
|
||
require(['jquery', 'narrativeConfig', 'narrativeLogin', 'preact_debug'], ($, Config, Login) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How does this differ from preact
? Is it preferred over that?
.gallery h2 { | ||
} | ||
|
||
|
||
.gallery h3 { | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are the empty ones necessary?
* @param {string} featureName The name of the given feature. | ||
* @returns {boolean} Whether the given feature is enabled | ||
*/ | ||
function isFeatureEnabled(featureName) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should figure out all enabled features in the factory constructor, and only do it once per session. Right now, if I make a cell, then delete the slug with the feature, and make another cell, they'll behave differently, which is kinda weird.
Also, it sorta hammers the browser to look up this stuff every time a new Runtime is needed.
console.warn('Already the first cell!'); | ||
return; | ||
} | ||
Jupyter.notebook.move_cell_up(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why use move_cell_up
/ down
here and move_selection_up / down above?
move_cell_up
throws a deprecation warning anyway, and with the other changes, might as well change to move_selection_*
here.
@@ -243,6 +414,24 @@ define([ | |||
} | |||
} | |||
|
|||
const extraIcon = (() => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What icon is this?
'preactComponents/RotatedTable', | ||
|
||
// for effect | ||
'css!./AddServiceWidgetDataViewer.css', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this needed? Shouldn't it just be part of the CSS stack?
const { Component } = preactCompat; | ||
const html = htm.bind(h); | ||
|
||
class ErrorAlert extends Component { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be ideal to go with functional components to be inline with Europa. I get that this would be a lot of changes, so it doesn't have to be done here. I also foresee having to do a lot of work to migrate to a new Narrative codebase (in pure React / TS) in the not-too-distant future, so maybe don't worry about it now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Noting that any major narrative codebase changes will be to support the new versions of Jupyter Notebook / Jupyterlab
delete constantParams.service_module_name; | ||
delete constantParams.widget_name; | ||
|
||
const params = Object.assign({}, constantParams, { ref }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not just set constantParams[ref] = ref, and use constantParams below?
Quality Gate failedFailed conditions See analysis details on SonarQube Cloud Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE |
Description of PR purpose/changes
Changes should not affect anything other than service widgets. Some changes cut across the codebase, but by far most changes only affect the new
nbextensions/serviceWidget
cell.Below is an extensive write-up of the changes:
Changes to Enable Service Widgets in the Narrative
This document summarizes changes required to the Narrative to enable the usage of service widgets.
Summary of Changes
nbextensions/serviceWidget
,src/biokbase/cells/service_widget.py
,kbase-extension/scss/partials/_serviceWidgetCell.scss
,pythonInterop.js
kbase-extension/static/kbase/js/util/cellSupport/
hasCellBase.js
andCellManager.js
, moving sharable cell code out of the specific implementationcellUtils.js
to make life a bit easier.preact
andhtm
dependencies inpackage.json
,narrative_paths.js
kbase-extension/static/kbase/js/preactComponents
-ErrorAlert
,Loading
, etc.nbextensions/serviceWidgetCell/util/SendCannel
andnbextensions/serviceWidgetCell/util/ReceiveChannel
to provide for serviceWidgetCell <-> serviceWidget comm through the window"message"
event, aka,postMessage()
.appCellWidget.js
kbaseNarrativeWorkspace.js
andnarrativeViewers.js
.AddServiceWidget
,AddServiceWidgetData
classes, which render bits forbootstrapDialog.js
bootstrapDialog
to add a size option, and added support the'show.bs.modal'
eventkbaseNarrative
to add developer menu dialogs and developer detectionnarrative_header.html
Details
Wherein we shall dive deeper into the following areas of implementation:
Implement service widget cell
The service widget cell is implemented as a notebook extension in the directory
nbextensions/serviceWidgetCell
.Python and Javascript injection
In addition to the files in this directory, python injection support is required for a new cell type.
pythonInterop.py
gains a new function,buildServiceWidgetRunner
, to generate the Python snippet for the cell. This snippet relies upon the implementation in the new functionrender_app
in a new modulebiokbase.cells.service_widget
. This function,in turn, generates the Javascript to be injected into the code cell output area. Finally, the injected Javascript invokes theRoot
widget innbextensions/serviceWidgetCell/widgets/Root.js
, from which the rest of the components are rendered."cell" implementation
It is worth noting that the cell implementation is divided into two main parts - the notebook cell implementation, which knows how to control the cell itself, and the widget implementation.
The top level of the cell's Javascript implementation is in in
nbextensions/serviceWidgetCell/ServiceWidgetCell.js
andutils/cellSupport/CellManager.js
.ServiceWidgetCell
is a subclass ofCellBase
inutil/cellSupport/CellBase.js
, which provides the bulk of the logic.The "manager" is responsible for integration with the notebook extension mechanism and notebook events. The "cell" is responsible for per-cell KBase behavior as well as specific behavior for the service widget cell.
"widget" implementation
The cell ui, what we think of as the "widget", begins life in the
Root
widget innbextensions/serviceWidgetCell/widgets/Root.js.
TheRoot
widget is primarily responsible for bridging between the Narrative and the service widget implementation. It prepares props for and then insertscomponents/Main.js
, which is implemented inpreact
inhtm
and provides a react componentMain
.As mentioned above, the cell's widget starts with
nbextensions/serviceWidgetCell/widgets/Root.js
. This is simply reponsible for inserting the widget into the DOM and rendering the top level react widget.This top level react component,
nbextensions/serviceWidgetCell/components/Main.js
, contains most of the logic to implement the service widget, including communication with the widget (via postMessage), preparation of widget parameters, and insertion of the component which implements the iframe.Finally a component implements the specially-formed iframe in
nbextensions/serviceWidgetCell/components/IFrame.js
. This is essentially a simple wrapper which, given a set of props, knows how to create the appropriate iframe markup.Communication between Narrative and embedded service widget app
A service widget cell supports communication with a service widget app embedded in an iframe. To facilitate this, a pair of classes,
SendChannel
innbextensions/serviceWidgetCell/SendChannel.js
andReceiveChannel
innbextensions/serviceWidgetCell/ReceiveChannel.js
are utilized. These are based on the same class I've used for years in kbase-ui to facilitate communication with the plugins using "window messages" via the postMessage api. However, unlike the kkbase-ui plugin implementation, this requires two channels (using the same channel id). This is because the service widget may be running on a different host than the Narrative, and browser security policy prevents Javascript in one window from listening for messages on another window if their origins differ. This will be true in prod.So, the comunication is split into a sender and receiver in order to be able to handle cross-origin communication.
The Narrative <-> Service Widget Cell communication is required to implement the invocation of service widgets, as well as additional features.
Additional features include:
More advanced uses include:
Miscellaneous Support Javascript
With
preact
available, a few general purpose components were added -Loading
,ErrorAlert
, andRotatedTable
.Basically, any repeated code was moved into a separate component.
The
Loading
component shows a loaindg spinner with an optional message. TheErrorAlert
shows a simple error message in an error alert, andRotatedTable
implements a simple two-column table, in which the first column is the header, and the second the value.In summary, all files added or changed to implement it
kbase-extension
static
kbase
js
common
pythonInterop.js
util
cellSupport
CellBase.js
CellManager.js
preactComponents
ErrorAlert.css
ErrorAlert.js
Loading.js
Loading.styles.js
nbextension
serviceWidgetCell
components
IFrame.css
IFrame.js
Main.css
Main.js
util
ReceiveChannel.js
SendChannel.js
widgets
Root.css
Root.js
Main
component with parameters passed from the cell.constants.js
main.js
ServiceWidgetCellManager.js
readme.md
ServiceWidgetCell.css
ServiceWidgetCell.js
Service Widget as Data Object Viewer
Enabling service widgets to serve as an object viewer requires changes to the existing Javascript module
widgets/narrative_core/kbaseNarrativeWorkspace.js,
fixes towidgets/narrative_core/narrativeViewers.js
, and, for supported types, modifications to viewer specifications inhttps://github.com/kbase/NarrativeViewers
.Javascript changes
In
kbaseNarrativeWorkspace.js
, thebuildViewerCell
kbwidget method is modified to determine the narrative viewer before inserting the data cell. In the current implementation of data cells, which have not been touched, the viewer is determined by the python code. However, in order to insert a serviceWidget cell rather than a data cell, we need to inspect the viewer spec first. This is because we need to determine whether the requested viewer should be served as a dynamic service widget, or let the existing mechanism handle it. And we need to do this before inserting the cell.The narrative viewer specs are cached, although the first time a data object is inserted into the Narrative in a given session, there will be some delay as it is fetched from the Narrative Method Store.
buildViewerCell
uses the existingloadViewerInfo
function in thenarrativeViewers
module. The relevant function,loadViewerInfo
, was not used in the codebase. I suspect it was used at one point, and then the functionality moved into the backend python handler for the data cell.loadViewerInfo
was updated to utilize the app panel's version tag, so that one can utilize adev
-tagged viewer. This allows us to have existing viewers operate as normal through therelease
tag, but if a user (in CI at least, when we are testing) selects thedev
tag, use a newer narrative viewer spec that has been updated to invoke a service widget. (This is of course contingent upon how theNarrativeViewers
"app" is managed via the catalog.)The viewer spec's
widget.output
property contains a special value for service widgetsServiceWidget
. It acts as a pseudo-widget, as it does not correspond to an actual AMD viewer module like the extant data widget mechanism does. It is used for conditionally branching to code which prepares and invokes a service widget, as opposed to a standard viewer.The service widget itself is agnostic with respect to how the service widget will be used. We don't want to put viewer logic into the service widget code itself. Thus,
buildViewerCell
is reponsible for packaging up the relevant widget invocation information, based on the viewer spec and the target object, so that the service widget cell can serve as a viewer cell. This informaiton includes the module name for the dynamic service, the widget name within the dynamic service, and the parameters, specifically the objectref
.Service Widget NarrativeViewer Changes
To enable a service widget to serve as a data object viewer for a given type, the viewer spec for the type must be modified in a specific manner.
spec.json
'swidgets.output
must be"ServiceWidget"
spec.json
must provide the module and widget names:constant_value
with atarget_property
of"service_module_name"
constant_value
with atarget_property
of"widget_name"
For example,
Note that no parameters are required. The data object's ref is automatically generated within
buildViewerCell
and passed to the widget. Thus, if you are modifying an existing viewer spec to use a service widget, you may safely remove any parameters.The
display.yaml
requires no changes to support service widgets, other than any description changes that may reflect changed functionality in the viewer. Of course, if parameters were removed fromspec.json
, they need to be removed fromdisplay.yaml
as well.Service Widget as App Output Viewer
Service widgets may also serve as viewers for app output.
As for data viewers, enabling service widgets requires changes to the existing mechanism for handling app output.
This functionality resides within the app cell itself - specifically the
createOutputCell
function innbextensions/appCell2/appCellWidget.js
.Similar to the data viewer cell, the implementation of the output viewer hinges on interceding before the cell type currently handling app output is inserted into the Narrative.
And also similar to the data viewer cell, the app output specification requires a specific format to indicate that a service widget should be used, and provides the service widget's dynamic service module name and widget name.
Summary of file changes
nbextensions/appCell2/appCellWidget.js
modified to support inserting a service widget if the app spec specifies itkbase-extension/static/kbase/js/util/icon.js
modified to add an app output icon based on the app icon itselfJavascript Changes
In the app cell execution lifecycle, an output cell may be inserted after the app has successfully completed. Whether to insert an output widget, which one to insert, and what parameters to provide it are all specified in the ui configuration for the app itself. We'll discuss that in the section below.
It is in the app cell implementation that the output cell is inserted into the Narrative, just below the app cell itself. The code responsible for this is in the
createOutputCell
function innbextensions/appCell2/appCellWidget.js
.The code previously had simply inserted an output cell. To support inserting a widget cell instead required few changes, as all relevant information was already available. The widget name is inspected, and if it matches
"ServiceWidget"
will generate aserviceWidget
cell specification rather than anoutput
widget.Icon Changes
Icon support in
jsutils/icons.js
was extended to provide support for an "app output" icon. The current app output icon is a left-pointing arrow. In order to better associate the output cell with the app cell that generated it, the icon for app output viewers implemented as service widgets is a compound icon with the original app icon on the left, and on the right a "right-pointing" arrow.This change is speculative. I feel we need a better way of associating an output cell with it's "parent" app cell, but I'm not sure this is it.
App Requirements
As described above, the Javascript implementation of app output cell injection is straightforward, requiring few effective lines of code to be changed or added. These changes, however, rely upon a specific structure in the app's ui configuration. These changes are very similar to that for data viewers.
In the
ui/narrative/methods/APP_NAME
(whereAPP_NAME
is the name of the app you are implementing) directory ofhttps://github.com/kbase/NarrativeViewers
, thespec.json
file has the following requirements:widgets.output
must be"ServiceWidget"
behavior.service-mapping.output_mapping
must provide:constant_value
with atarget_property
of"service_module_name"
constant_value
with atarget_property
of"widget_name"
target_property
"title"
target_property
"subtitle"
All other outputs, other than the module name, widget name, title, and subtitle, are passed directly to the service widget as parameters.
For example, from an example app in CI (https://github.com/eapearson/eapearsonNarrativeViewersTest):
Developer Tools
In early development, in the days before the data viewer and app output integrations had been added, a developer tool was created to enable direct insertion of service widgets into a Narrative.
Feature detection
The developer tool is enabled through the existing feature detection mechanism, with one enhancement - supplying a set of enabled features in the URL. This addition allows one to enable a feature on the fly, rather than having to modify
config/features.json
(or use Jupyter userSettings - but I don't know if we support it).The code for supporting feature detection and the developer feature in particular, is present in
common/ui.js
. However, the UI object requires a DOM node and where we need feature detection it doesn't make any sense to pass a DOM node to the UI object.So the necessary code was moved to
common/runtime.js
, which seems a more appropriate home, as the Runtime object does not require a DOM node. A few other small changes were made to the runtime module, for the sake of simplifying the code and module.The existing code in
ui.js
was refactored to call the code inruntime.js
. I looked for existing usages of the feature detection code, and it wasn't fully clear how it was used, although it is not used widely, and some of the code that uses it is not currently in use.Below are the existing features and their usage. Some use the features configuration directly and are unafected by the feature api changes. Such calls look like
Config.get('features').staticNarratives
.Two utilize the feature api calls, but appear to be unused. Well,
isDeveloper()
andifAdvanced()
are utilized in a few locations in the view cell widget code, but the view cell widget is not used.ifAdvanced() used in viewCellWidget but code using it is not displayed
config('features.developer') also used in viewCellWidget.js, code using them is not displayed
In our case, the feature we are interested in is
"developer"
, which we can enable in the URLhttps://ci.kbase.us/narrative/71571?features=developer
Or in
kbase-extension/static/kbase/config/feature-config.json
:Ad-hoc service widget tool
In developer mode, a new "developer" menu item appears on the top horizontal menu. It drops down two menu items:
Selecting either of these will display a dialog box in which the service widget parameters are entered.
Implementing this involved the following file changes:
kbase-extension/kbase_templates/narrative_header.html
adds the new dropdown menu. It is hidden by default, will be shown if the narrative is loaded with the 'developer' flag setkbase-extension/static/kbase/js/kbaseNarrative.js
provides support for enabling the menu, as well as handling the menu items, as it does for other header menu items.static/kbase/js/util/bootstrapDialog.js
received an update to support both the dialogsize
as well as theshow.bs.modal
event. These changes are required in order to show and handle the service widget developer tool pop-ups.Other developer affordances
In addition, because I needed to insert and re-run service widget cells repeatedly, and manipulate cells extensively, I added a few conveniences that are enabled in developer mode:
Jira Ticket / Issue
Related Jira ticket: https://kbase-jira.atlassian.net/browse/DATAUP-X
DATAUP-69 Adds a PR template
)Testing Instructions
Dev Checklist:
Updating Version and Release Notes (if applicable)