I'm working on creating a server-side extension in JupyterLab and have been searching for quite a while for a way to get the currently active notebook name inside my index.ts file. I found this existing extension that gets the currently active tab name with ILabShell and sets the browser tab to have the same name. In my case I'll be triggering the process with a button on the notebook toolbar so the active tab will always be a notebook. However, when I try to use the code in the activate section of my extension, I get TypeError: labShell.currentChanged is undefined where labShell is an instance of ILabShell. That extension doesn't support JupyterLab 3.0+ so I believe that's part of the problem. However, currentChanged for ILabShell is clearly defined here. What if anything can I change to make it work? Is there another way to accomplish what I'm trying to do? I'm aware of things like this to get the notebook name inside the notebook but that's not quite what I'm trying to do.
Windows 10,
Node v14.17.0,
npm 6.14.13,
jlpm 1.21.1,
jupyter lab 3.0.14
I'm using this example server extension as a template: https://github.com/jupyterlab/extension-examples/tree/master/server-extension
index.ts file from the existing extension to get the current tab name:
import {
ILabShell,
JupyterFrontEnd,
JupyterFrontEndPlugin
} from '#jupyterlab/application';
import { Title, Widget } from '#lumino/widgets';
/**
* Initialization data for the jupyterlab-active-as-tab-name extension.
*/
const extension: JupyterFrontEndPlugin<void> = {
id: 'jupyterlab-active-as-tab-name',
autoStart: true,
requires: [ILabShell],
activate: (app: JupyterFrontEnd, labShell: ILabShell) => {
const onTitleChanged = (title: Title<Widget>) => {
console.log('the JupyterLab main application:', title);
document.title = title.label;
};
// Keep the session object on the status item up-to-date.
labShell.currentChanged.connect((_, change) => {
const { oldValue, newValue } = change;
// Clean up after the old value if it exists,
// listen for changes to the title of the activity
if (oldValue) {
oldValue.title.changed.disconnect(onTitleChanged);
}
if (newValue) {
newValue.title.changed.connect(onTitleChanged);
}
});
}
};
export default extension;
My index.ts file:
import {
ILabShell,
JupyterFrontEnd,
JupyterFrontEndPlugin
} from '#jupyterlab/application';
import { ICommandPalette } from '#jupyterlab/apputils';
import { ILauncher } from '#jupyterlab/launcher';
import { requestAPI } from './handler';
import { ToolbarButton } from '#jupyterlab/apputils';
import { DocumentRegistry } from '#jupyterlab/docregistry';
import { INotebookModel, NotebookPanel } from '#jupyterlab/notebook';
import { IDisposable } from '#lumino/disposable';
import { Title, Widget } from '#lumino/widgets';
export class ButtonExtension implements DocumentRegistry.IWidgetExtension<NotebookPanel, INotebookModel> {
constructor(app: JupyterFrontEnd) {
this.app = app;
}
readonly app: JupyterFrontEnd
createNew(panel: NotebookPanel, context: DocumentRegistry.IContext<INotebookModel>): IDisposable {
// dummy json data to test post requests
const data2 = {"test message" : "message"}
const options = {
method: 'POST',
body: JSON.stringify(data2),
headers: {'Content-Type': 'application/json'
}
};
// Create the toolbar button
let mybutton = new ToolbarButton({
label: 'Measure Energy Usage',
onClick: async () => {
// POST request to Jupyter server
const dataToSend = { file: 'nbtest.ipynb' };
try {
const reply = await requestAPI<any>('hello', {
body: JSON.stringify(dataToSend),
method: 'POST'
});
console.log(reply);
} catch (reason) {
console.error(
`Error on POST /jlab-ext-example/hello ${dataToSend}.\n${reason}`
);
}
// sample POST request to svr.js
fetch('http://localhost:9898/api', options);
}
});
// Add the toolbar button to the notebook toolbar
panel.toolbar.insertItem(10, 'MeasureEnergyUsage', mybutton);
console.log("MeasEnerUsage activated");
// The ToolbarButton class implements `IDisposable`, so the
// button *is* the extension for the purposes of this method.
return mybutton;
}
}
/**
* Initialization data for the server-extension-example extension.
*/
const extension: JupyterFrontEndPlugin<void> = {
id: 'server-extension-example',
autoStart: true,
optional: [ILauncher],
requires: [ICommandPalette, ILabShell],
activate: async (
app: JupyterFrontEnd,
palette: ICommandPalette,
launcher: ILauncher | null,
labShell: ILabShell
) => {
console.log('JupyterLab extension server-extension-example is activated!');
const your_button = new ButtonExtension(app);
app.docRegistry.addWidgetExtension('Notebook', your_button);
// sample GET request to jupyter server
try {
const data = await requestAPI<any>('hello');
console.log(data);
} catch (reason) {
console.error(`Error on GET /jlab-ext-example/hello.\n${reason}`);
}
// get name of active tab
const onTitleChanged = (title: Title<Widget>) => {
console.log('the JupyterLab main application:', title);
document.title = title.label;
};
// Keep the session object on the status item up-to-date.
labShell.currentChanged.connect((_, change) => {
const { oldValue, newValue } = change;
// Clean up after the old value if it exists,
// listen for changes to the title of the activity
if (oldValue) {
oldValue.title.changed.disconnect(onTitleChanged);
}
if (newValue) {
newValue.title.changed.connect(onTitleChanged);
}
});
}
};
export default extension;
There are two ways to fix it and one way to improve it. I recommend using (2) and (3).
Your order of arguments in activate is wrong. There is no magic matching of argument types to signature function; instead arguments are passed in the order given in requires and then optional. This means that you will receive:
...[JupyterFrontEnd, ICommandPalette, ILabShell, ILauncher]
but what you are expecting is:
...[JupyterFrontEnd, ICommandPalette, ILauncher, ILabShell]
In other words, optionals are always at the end. There is no static type check so this is a common source of mistakes - just make sure you double check the order next time (or debug/console.log to see what you are getting).
Actually, don't require ILabShell as a token. Use the ILabShell that comes in app.shell instead. This way your extension will be also compatible with other frontends built using JupyterLab components.
shell = app.shell as ILabShell
(optional improvement) install RetroLab as a development-only requirement and use import type (this way it is not a runtime requirement) to ensure compatibility with RetroLab:
import type { IRetroShell } from '#retrolab/application';
// ... and then in `activate()`:
shell = app.shell as ILabShell | IRetroShell
to be clear: not doing so would not make your extension incompatible; what it does is ensures you do not make it incompatible by depending on lab-specific behaviour of the ILabShell in the future.
So in total it would look like:
import {
ILabShell,
JupyterFrontEnd,
JupyterFrontEndPlugin
} from '#jupyterlab/application';
import type { IRetroShell } from '#retrolab/application';
// ...
const extension: JupyterFrontEndPlugin<void> = {
id: 'server-extension-example',
autoStart: true,
optional: [ILauncher],
requires: [ICommandPalette],
activate: async (
app: JupyterFrontEnd,
palette: ICommandPalette,
launcher: ILauncher | null
) => {
let shell = app.shell as ILabShell | IRetroShell ;
shell.currentChanged.connect((_, change) => {
console.log(change);
// ...
});
}
};
export default extension;
Related
While uploding the image i'm getting below error at server console.
I20200123-18:57:34.751(5.5)? Exception while invoking method 'collections.images.insert' TypeError: Cannot read property 'insert' of undefined
I20200123-18:57:34.753(5.5)? at MethodInvocation.collections.images.insert (imports/api/collections/methods.js:20:31)
I20200123-18:57:34.753(5.5)? at maybeAuditArgumentChecks (packages/ddp-server/livedata_server.js:1771:12)
I20200123-18:57:34.753(5.5)? at DDP._CurrentMethodInvocation.withValue (packages/ddp-server/livedata_server.js:719:19)
I20200123-18:57:34.754(5.5)? at Meteor.EnvironmentVariable.EVp.withValue (packages/meteor.js:1234:12)
I20200123-18:57:34.754(5.5)? at DDPServer._CurrentWriteFence.withValue (packages/ddp-server/livedata_server.js:717:46)
I20200123-18:57:34.754(5.5)? at Meteor.EnvironmentVariable.EVp.withValue (packages/meteor.js:1234:12)
I20200123-18:57:34.754(5.5)? at Promise (packages/ddp-server/livedata_server.js:715:46)
I20200123-18:57:34.755(5.5)? at new Promise (<anonymous>)
I20200123-18:57:34.755(5.5)? at Session.method (packages/ddp-server/livedata_server.js:689:23)
I20200123-18:57:34.755(5.5)? at packages/ddp-server/livedata_server.js:559:43
Here, I'm providing my code snippets for your referance.
Collection constructor server Code :
import { FilesCollection } from 'meteor/ostrio:files';
const Collections_Images = new FilesCollection({
collectionName: 'collections_images',
storagePath: 'uploads/Collections-Images',
allowClientCode: false,
onBeforeUpload(file) {
// Allow upload files under 10MB, and only in png/jpg/jpeg formats
if (file.size <= 10485760 && /png|jpg|jpeg/i.test(file.extension)) {
return true;
}
return 'Please upload image, with size equal or less than 10MB';
}
});
export default Collections_Images;
Created method Code :
import { Meteor } from 'meteor/meteor';
import { Collections_Images } from './collections_img.js';
Meteor.methods({
'collections.images.insert'(images) {
return Collections_Images.insert({
file : images,
streams: 'dynamic',
chunkSize: 'dynamic',
})
},
});
Client side js code:
"change #myFileInput" : function(event){
const images = event.currentTarget.files[0];
Meteor.call('collections.images.insert',images, (error) => {
if(error){
alert("collection image insert : "+error.message);
}
else{
images=null;
}
});
}
Thanks in advance.
You export default from './collections_img.js'
But you import it like this:
import { Collections_Images } from './collections_img.js';
You need to import it like this
import Collections_Images from './collections_img.js';
And .insert() method is on client side code, there is no need of sending file over the Meteor.method
I'm trying to get my head around Meteor's Tracker.autorun and Tracker.dependancy features.
I'm trying to do something that seems simple in my mind but I'm struggling to execute.
I have a server-side function, that I register as a method:
let count = 0
setInterval(()=>{
count ++
return count
}, 1000)
export default count
Register as a method:
import count from './setIntervarl'
Meteor.methods({
getData:function() {
return count
}
});
And then call up on the client side:
import { Template } from 'meteor/templating';
import { ReactiveVar } from 'meteor/reactive-var';
import { Tracker } from 'meteor/tracker'
import './main.html';
// Setup reactive variable
rv1 = new ReactiveVar(9)
Meteor.call('getData', function(error, results) {
if(error){
console.log("error:"+error);
} else {
rv1.set(results)
}
});
// Display the output from reactiveVar
Template.someData.helpers({
someData: function() {
return rv1.get();
}
})
Can someone please show me how to use Tracker.autorun or Tracker.dependancy so that my UI updates with interval that is set in my server-side function
I'm having really trouble getting this working.
Many thanks
There will be no reactivity out of the box here. Meteor methods are not reactive but just a wrapped ddp call to a server (rpc-) endpoint that returns something.
In order to gain reactive data from the server, you need to subscribe to a publication. If you want only this counter being published, you may create a collection with a single document and publish it.
imports/CountCollection.js (both)
export const CountCollection = new Mongo.Collection('myCounter')
server/counter.js (server)
import { CountCollection } from '../imports/CountCollection'
let counterDocId
Meteor.startup(() => {
// optional: clear the collection on a new startup
// this is up to your use case
// CountCollection.remove({})
// create a new counter document
counterDocId = CountCollection.insert({ count: 0 })
// use the Meteor.setInterval method in order to
// keep the Meteor environment bound to the execution context
// then update the counter doc each second
Meteor.setInterval(function () {
CountCollection.update(counterDocId, { $inc: { count: 1 } })
}, 1000)
})
// Now we need a publication for the counter doc.
// You can use the `limit` projection to restrict this to a single document:
Meteor.publish('counterDoc', function () {
if (!counterDocId) this.ready()
return CountCollection.find({ _id: counterDocId }, { limit: 1 })
})
Now you can subscribe to this publication and get reactive updates to the document:
client/someData.js (client)
import { CountCollection } from '../imports/CountCollection'
import { Template } from 'meteor/templating';
import { ReactiveVar } from 'meteor/reactive-var';
import { Tracker } from 'meteor/tracker'
import './main.html';
// Setup reactive variable
const reactiveCounter = new ReactiveVar(0)
const counterSubscription = Meteor.subscribe('counterDoc')
Template.someData.onCreated(() => {
const instance = this
instance.autorun(() => {
// counterSubscription.ready() will re-called
// when the publication released a new cursor
// which causes the autorun to re-run = reactivity
if (counterSubscription.ready()) {
// there is only 1 doc published, so no query require
const counterDoc = CountCollection.findOne()
reactiveCounter.set(counterDoc && counterDoc.count)
}
})
})
// Display the output from reactiveVar
Template.someData.helpers({
someData: function() {
return reactiveCounter.get()
}
})
Sidenote:
don't forget to get the paths and imports correct
Readings:
https://docs.mongodb.com/manual/reference/operator/update/inc/
https://docs.meteor.com/api/timers.html#Meteor-setInterval
I have a dotnet core api that returns a FileContentResult..
return new FileContentResult(bytes, contentType)
{
FileDownloadName = Path.GetFileName(request.Filename)
};
Via postman I can read out the image perfectly fine. Now I want to read the image, via the aurelia fetch client, and show it in my html view. This is my function to retrieve the image from the api.
public image(filename: string) {
return this.http.fetch(AppConfiguration.base_url + 'assets/image',
{
method: 'post',
body: json({
filename: filename
})
});
}
I've tried to convert the blob in the response with this value converter. But I can't get that to work
Converter:
export class BlobToUrlValueConverter {
public toView(blob) {
return URL.createObjectURL(blob);
}
}
Viewmodel:
export class Dashboard {
public blob: any;
constructor(
public assets_service: AssetsService
) { }
async attached() {
let response = await this.assets_service.image('test.png');
this.blob = response.blob();
}
}
View
<div if.bind="blob">
${ blob | blobToUrl }
</div>
I'm not sure this is the right approach. Also not sure how handle the async request part of it either. What is the best way to get that image response to show in the html view? Lets say via a img tag?
I was close. Here is how I got the image to show.
Viewmodel:
export class Dashboard {
public url: string;
constructor(
public assets_service: AssetsService
) { }
async attached() {
let blob = await this.assets_service.image('test.png')
.then(response => response.blob());
this.url = URL.createObjectURL(blob);
}
}
View:
<div if.bind="url">
<img src.bind="url">
</div>
EDIT:
Found a better solution using parts written above:
The exported function that does the call (for reusability on both ts and html sides):
export function image_request(filename: string): Promise<Response> {
let http = new Http();
return http.fetch(<your-url-that-fetches-the-image>,
{
method: 'post',
body: json({
filename: filename
})
});
}
Value converter that uses above function
import { image_request } from './AssetsRequests';
export class ImageRequestValueConverter {
public toView(filename: string) {
return image_request(filename);
}
}
The important and most awesome part of the solution. Many thanks to http://www.sobell.net/aurelia-async-bindings/
for getting me on my way. You can override the binding behaviour. You can use this override to process async
Promise in a view in combination with a value converter.
export class AsyncImageBindingBehavior {
public bind(binding, source): void {
binding.originalupdateTarget = binding.updateTarget;
binding.updateTarget = (target) => {
// When we have a promise
if (typeof target.then === 'function') {
// Set temp value to loading so we know its loading
binding.originalupdateTarget('Loading...');
// Process the promise
target
.then(response => response.blob())
.then(blob => binding.originalupdateTarget(
URL.createObjectURL(blob)
));
}
else {
binding.originalupdateTarget(target);
}
};
}
unbind(binding) {
binding.updateTarget = binding.originalupdateTarget;
binding.originalupdateTarget = null;
}
}
Finally the view is very simple
<img src="${ 'test.png' | imageRequest & asyncImage }">
Yo! I'm using Redux and Normalizr. The API I'm working with sends down objects that look like this:
{
name: 'Foo',
type: 'ABCD-EFGH-IJKL-MNOP'
}
or like this
{
name: 'Foo2',
children: [
'ABCD-EFGH-IJKL-MNOP',
'QRST-UVWX-YZAB-CDEF'
]
}
I want to be able to asynchronously fetch those related entities (type and children) when the above objects are accessed from the state (in mapStateToProps). Unfortunately, this does not seem to mesh with the Redux way as mapStateToProps is not the right place to call actions. Is there an obvious solution to this case that I'm overlooking (other than pre-fetching all of my data)?
Not sure that I have correctly understood your use-case, but if you want to fetch data, one simple common way is to trigger it from a React component:
var Component = React.createClass({
componentDidMount: function() {
if (!this.props.myObject) {
dispatch(actions.loadObject(this.props.myObjectId));
}
},
render: function() {
const heading = this.props.myObject ?
'My object name is ' + this.props.myObject.name
: 'No object loaded';
return (
<div>
{heading}
</div>
);
},
});
Given the "myObjectId" prop, the component triggers the "myObject" fetching after mounting.
Another common way would be to fetch the data, if it's not already here, from a Redux async action creator (see Redux's doc for more details about this pattern):
// sync action creator:
const FETCH_OBJECT_SUCCESS = 'FETCH_OBJECT_SUCCESS';
function fetchObjectSuccess(objectId, myObject) {
return {
type: FETCH_OBJECT_SUCCESS,
objectId,
myObject,
};
}
// async action creator:
function fetchObject(objectId) {
return (dispatch, getState) => {
const currentAppState = getState();
if (!currentAppState.allObjects[objectId]) {
// fetches the object if not already present in app state:
return fetch('some_url_.../' + objectId)
.then(myObject => (
dispatch(fetchObjectSuccess(objectId, myObject))
));
} else {
return Promise.resolve(); // nothing to wait for
}
};
}
I'm using aurelia-validate and my validation works fine if I use variables, but I need it to validate properties of an object rather than a variable:
Here's what works:
import {Validation} from 'aurelia-validation';
import {ensure} from 'aurelia-validation';
import {ItemService} from './service';
export class EditItem {
static inject() {
return [Validation, ItemService];
}
#ensure(function(it){
it.isNotEmpty()
.hasLengthBetween(3,10);
})
name = '';
#ensure(function(it){
it.isNotEmpty()
.hasMinLength(10)
.matches(/^https?:\/\/.{3,}$/) //looks like a url
.matches(/^\S*$/); //no spaces
})
url = '';
constructor(validation, service) {
this.validation = validation.on(this);
this.service = service;
}
activate(params){
return this.service.getItem(params.id).then(res => {
console.log(res);
this.name = res.content.name; //populate
this.url = res.content.url;
});
}
update() {
this.validation.validate().then(
() => {
var data = {
name: this.name,
url: this.url
};
this.service.updateItem(data).then(res => {
this.message = "Thank you!";
})
}
);
}
}
Here's what I'm trying to do (but doesn't work)...also I'm not sure if it's better to keep the properties on the class or have a property called this.item which contains the properties (this is the typical angular way):
import {Validation} from 'aurelia-validation';
import {ensure} from 'aurelia-validation';
import {ItemService} from './service';
export class EditItem {
static inject() {
return [Validation, ItemService];
}
#ensure(function(it){
it.isNotEmpty()
.hasLengthBetween(3,10);
})
this.item.name; //no assignment here should happen
#ensure(function(it){
it.isNotEmpty()
.hasMinLength(10)
.matches(/^https?:\/\/.{3,}$/) //looks like a url
.matches(/^\S*$/); //no spaces
})
this.item.url; //no assignment?
constructor(validation, service) {
this.validation = validation.on(this);
this.service = service;
this.item = null;
}
activate(params){
return this.service.getItem(params.id).then(res => {
console.log(res);
this.item = res.content; //populate with object from api call
});
}
update() {
this.validation.validate().then(
() => {
var data = {
name: this.item.name,
url: this.item.url
};
this.service.updateItem(data).then(res => {
this.message = "Thank you!";
})
}
);
}
}
Can someone give me some guidance here on how to use a validator against an existing object (for an edit page)?
The validation works in all kinds of situations, but using the #ensure decorator can only be used to declare your rules on simple properties (like you found out).
Hence...
Option a: replace the ensure decorator with the fluent API 'ensure' method, this supports 'nested' or 'complex' binding paths such as:
import {Validation} from 'aurelia-validation';
import {ItemService} from './service';
export class EditItem {
static inject() {
return [Validation, ItemService];
}
constructor(validation, service) {
this.validation = validation.on(this)
.ensure('item.url')
.isNotEmpty()
.hasMinLength(10)
.matches(/^https?:\/\/.{3,}$/) //looks like a url
.matches(/^\S*$/)
.ensure('item.name')
.isNotEmpty()
.hasLengthBetween(3,10);
this.service = service;
this.item = null;
}
activate(params){
return this.service.getItem(params.id).then(res => {
console.log(res);
this.item = res.content; //populate with object from api call
});
}
update() {
this.validation.validate().then(
() => {
var data = {
name: this.item.name,
url: this.item.url
};
this.service.updateItem(data).then(res => {
this.message = "Thank you!";
})
}
);
}
}
Note: you can set up your validation even before item is set. Cool, no?
Option b: Since the validation rules are specific to the item, you could move your validation rules inside your item class using the #ensure decorator inside that class instead.
You can then set up validation in your VM after you've retrieved the item: this.validation = validation.on(this.item); or, your service can set up the validation when it returns your item to your VM and make it an intrinsic part of the model: item.validation = validation.on(item);
Option a is easiest and seems to match your experience. Option b is more maintainable, as the validation rules for your model will live on the model, not on the view-model. However if you go with option b, you might have to adjust your HTML a bit to make sure validation hints appear.
Use the .on method of the validator to apply your rules to object properties.
The example below is called after I retrieve an object named stock, it validates that the quantity is not empty and is numeric only. Hope this helps...
let stock = {
name: 'some name'
minimumQuantity: '1'
};
applyRules() {
ValidationRules
.ensure((m: EditStock) => m.minimumQuantity)
.displayName("Minimum Quantity")
.required()
.withMessage(`\${$displayName} cannot be blank.`)
.matches( /^[0-9]*$/)
.withMessage(`\${$displayName} must be numeric only.`)
.on(this.stock);
}