I am trying to simulate map-like behaviour in TypeScript and also get code completion of possible values. I am limited to TypeScript 1.8.
catalog.ts
export declare type CATALOG = 'CATALOG1' | 'CATALOG2' | 'CATALOG3';
export const CATALOGS: { [catalog: string]: CATALOG } = {
CATALOG1: 'CATALOG1',
CATALOG2: 'CATALOG2',
CATALOG3: 'CATALOG3'
};
example.ts
import { CATALOGS } from './catalog';
class MyClass {
catalogs = CATALOGS;
constructor() {
CATALOGS.CATALOG1; // error
this.catalogs.CATALOG1; // error
}
}
This results in following error:
Property 'CATALOG1' does not exist on type { [catalog: string]:
"CATALOG1" | "CATALOG2" | "CATALOG3" }
Can someone elaborate?
What you're describing isn't a "map-like behaviour", with a map you'll do something like:
CATALOGS.get("CATALOG1");
And that's basically what you defined with: { [catalog: string]: CATALOG }.
You can access it like this:
let a = CATALOGS["CATALOG1"];
If you want to access it as you did, then it should be:
interface Catalogs {
CATALOG1: CATALOG;
CATALOG2: CATALOG;
CATALOG3: CATALOG;
}
export const CATALOGS: Catalogs = {
CATALOG1: 'CATALOG1',
CATALOG2: 'CATALOG2',
CATALOG3: 'CATALOG3'
};
let a = CATALOGS.CATALOG1;
(code in playground)
Related
Why does this property in react CSS not work if it is of type CSSProperties? How can I get it to work with Properties<string | number> ?
export const fields: GridFieldsConfiguration[] = [
{
...defaultColDefs,
field: 'amInitials',
displayNameRule: 'Asset Manager',
flex: 1.1,
minWidth: 75,
cellStyle: (params: any): any => {
getCellStyle(params, 'amInactive')
}
}
];
const isDisabledbStyle = {
color: '#FF0000'
};
const getCellStyle = ((params: any, inactiveCol: string): CSSProperties => {
console.log(params);
if (params?.api?.getValue(inactiveCol, params.node) === true) {
return isDisabledbStyle;
} else {
return isDisabledbStyle;
}
}
);
Here are the types. cellStyle comes from CSSProperties which is an extension of CSS.Properties<string | number>.
export interface GridFieldConfiguration extends FieldConfiguration {
cellStyle?: CSSProperties;
}
export interface CSSProperties extends CSS.Properties<string | number> {
/**
* The index signature was removed to enable closed typing for style
* using CSSType. You're able to use type assertion or module augmentation
* to add properties or an index signature of your own.
*
* For examples and more information, visit:
* https://github.com/frenic/csstype#what-should-i-do-when-i-get-type-errors
*/
}
Here is Properties
export interface Properties<TLength = string | 0> extends StandardProperties<TLength>, VendorProperties<TLength>, ObsoleteProperties<TLength>, SvgProperties<TLength> {}
Sounds like you are using AG Grid, and trying to configure a cellStyleFunc for the cellStyle option of column definitions?
As shown in the linked documentation, it is indeed possible to provide a function that takes a params argument.
But it looks like in your case, you have an intermediate GridFieldsConfiguration custom type, that expects cellStyle to be of type Properties<string | number> (which is very probably actually React.CSSProperties), which does not accepts the function form, hence the error message.
If the rest of your code that handles GridFieldsConfiguration really expects CSSProperties and not the function form, then you would have to refactor it first, so that it can handle that form.
If all it does is to pass the cellStyle option to AG Grid, then you just need to improve the definition of GridFieldsConfiguration type. You can re-use the actual types grom AG Grid, e.g.:
import { AgGridColumnProps as ColDef } from "ag-grid-react";
export interface GridFieldsConfiguration {
cellStyle?: ColDef["cellStyle"];
}
But note that CSSProperties is actually not type-compatible with cellStyle. To fix it, simply remove the return type assertion on your getCellStyle function. If you want to ensure that the returned objects still resembles a CSS object, you can use the new satisfies operator:
const isDisabledbStyle = {
color: '#FF0000'
} satisfies CSSProperties;
const getCellStyle = (params: any, inactiveCol: string) => {
return isDisabledbStyle;
};
Playground Link
I am currently working on a bokeh application with continuous AND categorical data in the same Dataframe. Since the PointDrawTool of bokeh only comes with support for continuous data, I need to write a custom PointDrawTool myself. I followed the custom tools tutorial (https://docs.bokeh.org/en/2.4.1/docs/user_guide/extensions_gallery/tool.html) and changed the TypeScript code. I just copied the source code of the PointDrawTool (https://github.com/bokeh/bokeh/blob/branch-3.0/bokehjs/src/lib/models/tools/edit/point_draw_tool.ts) and fixed all the imports so that I do not run into any TypeScript Errors anymore. However, I always get a weird console error when loading the page. Additionally, the page remains completely blank and none of my widgets are showing.
Error in console: "Failed to repull session TypeError: this.properties[e] is undefined"
Code in draw_tool.py:
from bokeh.core.properties import Instance
from bokeh.io import output_file, show
from bokeh.models import ColumnDataSource, Tool
from bokeh.plotting import figure
from bokeh.util.compiler import TypeScript
output_file('tool.html')
TS_CODE = """
import {Keys} from "core/dom"
import {PanEvent, TapEvent, KeyEvent} from "core/ui_events"
import * as p from "core/properties"
import {GlyphRenderer} from "models/renderers/glyph_renderer"
import {EditTool, EditToolView, HasXYGlyph} from "models/tools/edit/edit_tool"
import {tool_icon_point_draw} from "styles/icons.css"
export class CustomDrawToolView extends EditToolView {
override model: CustomDrawTool
override _tap(ev: TapEvent): void {
const renderers = this._select_event(ev, this._select_mode(ev), this.model.renderers)
if (renderers.length || !this.model.add) {
return
}
const renderer = this.model.renderers[0]
const point = this._map_drag(ev.sx, ev.sy, renderer)
if (point == null)
return
// Type once dataspecs are typed
const glyph: any = renderer.glyph
const cds = renderer.data_source
const [xkey, ykey] = [glyph.x.field, glyph.y.field]
const [x, y] = point
this._pop_glyphs(cds, this.model.num_objects)
if (xkey) cds.get_array(xkey).push(x)
if (ykey) cds.get_array(ykey).push(y)
this._pad_empty_columns(cds, [xkey, ykey])
const {data} = cds
cds.setv({data}, {check_eq: false}) // XXX: inplace updates
}
override _keyup(ev: KeyEvent): void {
if (!this.model.active || !this._mouse_in_frame)
return
for (const renderer of this.model.renderers) {
if (ev.keyCode === Keys.Backspace) {
this._delete_selected(renderer)
} else if (ev.keyCode == Keys.Esc) {
renderer.data_source.selection_manager.clear()
}
}
}
override _pan_start(ev: PanEvent): void {
if (!this.model.drag)
return
this._select_event(ev, "append", this.model.renderers)
this._basepoint = [ev.sx, ev.sy]
}
override _pan(ev: PanEvent): void {
if (!this.model.drag || this._basepoint == null)
return
this._drag_points(ev, this.model.renderers)
}
override _pan_end(ev: PanEvent): void {
if (!this.model.drag)
return
this._pan(ev)
for (const renderer of this.model.renderers)
this._emit_cds_changes(renderer.data_source, false, true, true)
this._basepoint = null
}
}
export namespace CustomDrawTool {
export type Attrs = p.AttrsOf<Props>
export type Props = EditTool.Props & {
add: p.Property<boolean>
drag: p.Property<boolean>
num_objects: p.Property<number>
renderers: p.Property<(GlyphRenderer & HasXYGlyph)[]>
}
}
export interface CustomDrawTool extends CustomDrawTool.Attrs {}
export class CustomDrawTool extends EditTool {
override properties: CustomDrawTool.Props
override __view_type__: CustomDrawToolView
override renderers: (GlyphRenderer & HasXYGlyph)[]
constructor(attrs?: Partial<CustomDrawTool.Attrs>) {
super(attrs)
}
static {
this.prototype.default_view = CustomDrawToolView
this.define<CustomDrawTool.Props>(({Boolean, Int}) => ({
add: [ Boolean, true ],
drag: [ Boolean, true ],
num_objects: [ Int, 0 ],
}))
}
override tool_name = "Point Draw Tool XX"
tool_icon = tool_icon_point_draw
override event_type = ["tap" as "tap", "pan" as "pan", "move" as "move"]
override default_order = 2
}
"""
class CustomDrawTool(Tool):
__implementation__ = TypeScript(TS_CODE)
source = Instance(ColumnDataSource)
Usage in main.py
from custom_tools.draw_tool import CustomDrawTool
tools = config.TOOLS + [CustomDrawTool(source=data_model.data_X)]
p = figure(height=config.PLOT_HEIGHT, width=config.PLOT_WIDTH, tools=tools, output_backend="webgl",
**kw_figure)
Note: data_model.data_X is just a ColumnDataSource.
Hopefully someone can give me a hint on how to fix the problem and continue with writing my own Tool.
Thanks & best regards
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;
I have set routing and display the page according to user roles. For this i am using guard on route. I am extracting userRole from service in Appcomponent class and using set and get method in main-service file. Now problem is that before i get role, routing takes place and it navigate to wrong url as it doesn't have role by then. Tough from next call, it works properly. Let me share the code:-
1.Here is guard class:-
export class HomeGuard implements CanActivate {
constructor(private _router: Router,private mainService: MainService) {
}
canActivate(): boolean {
let userRoles:any;
alert('HomeGuard');
userRoles = this.mainService.getSavedUserRole();
//userRoles = ['Profile Manager','Operations','Shipper'];
alert('userRoles are here'+userRoles);
console.log('here in homeguard');
if(userRoles) {
if(userRoles.some(x => x === 'Shipper') || userRoles.some(x => x === 'Admin'))
return true;
}
this._router.navigate(['/notfound']);
return false;
}
}
Here is AppComponent where i am extracting userRole from service:-
export class AppComponent {
savedUserRoles:any;
constructor(private translate: TranslateService,private mainService: MainService) {
console.log('Environment config', Config);
// this language will be used as a fallback when a translation isn't found in the current language
translate.setDefaultLang(AppSettings.LNG_TYPE);
// the lang to use, if the lang isn't available, it will use the current loader to get them
translate.use(AppSettings.LNG_TYPE);
this.mainService.getCurrentUser().subscribe(result => {
this.savedUserRoles = JSON.parse(JSON.parse(result._body).Data).Roles;
console.log('sdfghj'+this.savedUserRoles);
this.mainService.setSavedUserRole(this.savedUserRoles);
});
}
}
Here is main-service where i have defined set and get method:-
setSavedUserRole(name: any) {
console.log('main'+name);
this._userRoles = name;
}
getSavedUserRole() {
return this._userRoles;
}
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);
}