Skip to content
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

Ydoc test #567

Draft
wants to merge 18 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -135,3 +135,6 @@ dev/*.ipynb
# Jest coverage reports and a side effect
coverage
junit.xml

# Interfaces generated from json-schema
src/workflows/_interface
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"build:labextension": "jupyter labextension build .",
"build:labextension:dev": "jupyter labextension build --development True .",
"build:lib": "tsc",
"build:schema:js": "json2ts -i src/workflows/schema -o src/workflows/_interface --no-unknownAny --unreachableDefinitions --cwd ./src/workflows/schema",
"clean": "jlpm clean:lib",
"clean:lib": "rimraf lib tsconfig.tsbuildinfo",
"clean:lintcache": "rimraf .eslintcache .stylelintcache",
Expand Down Expand Up @@ -74,6 +75,7 @@
"@mui/system": "^5.10.6",
"@types/react-dom": "^18.0.5",
"cronstrue": "^2.12.0",
"json-schema-to-typescript": "^15.0.3",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"tzdata": "^1.0.33"
Expand Down
50 changes: 50 additions & 0 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ import { NotebookJobsPanel } from './notebook-jobs-panel';
import { Scheduler } from './tokens';
import { SERVER_EXTENSION_404_JSX } from './util/errors';
import { MakeNameValid } from './util/job-name-validation';
import { WorkflowModelFactory } from './workflows/workflowModel';
import { WorkflowWidgetFactory } from './workflows/workflowWidgetFactory';

export namespace CommandIDs {
export const deleteJob = 'scheduling:delete-job';
Expand Down Expand Up @@ -194,10 +196,58 @@ function activatePlugin(
telemetryHandler: Scheduler.TelemetryHandler,
launcher: ILauncher | null
): void {
// Hardcoded boolean for testing. If true, set up workflow widget instead of scheduler UI
const showWorkflowsWidget = true;

const trans = translator.load('jupyterlab');
const api = new SchedulerService({});
verifyServerExtension({ api, translator });

if (showWorkflowsWidget) {
const WORKFLOW_FACTORY = 'Workflow Editor';
const WORKFLOW_CONTENT_TYPE = 'workflow';
const WORKFLOW_FILE_EXT = '.jwf';

// Register the workflow file type
app.docRegistry.addFileType({
name: WORKFLOW_CONTENT_TYPE,
displayName: 'Workflow File',
extensions: [WORKFLOW_FILE_EXT],
fileFormat: 'text',
contentType: 'file',
mimeTypes: ['application/json']
});

// Register the workflow model factory
const modelFactory = new WorkflowModelFactory();
app.docRegistry.addModelFactory(modelFactory);

// Register the workflow widget factory
const widgetFactory = new WorkflowWidgetFactory({
name: WORKFLOW_FACTORY,
modelName: modelFactory.name,
fileTypes: [WORKFLOW_CONTENT_TYPE],
defaultFor: [WORKFLOW_CONTENT_TYPE]
});
app.docRegistry.addWidgetFactory(widgetFactory);

// Create a new untitled .jwf file and open it
void app.commands
.execute('docmanager:new-untitled', {
type: 'file',
ext: '.jwf'
})
.then(model => {
if (model) {
void app.commands.execute('docmanager:open', {
path: model.path
});
}
});

return;
}

const { commands } = app;
const fileBrowserTracker = browserFactory.tracker;
const widgetTracker = new WidgetTracker<MainAreaWidget<NotebookJobsPanel>>({
Expand Down
21 changes: 21 additions & 0 deletions src/workflows/interfaces.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { DocumentChange, StateChange, YDocument } from '@jupyter/ydoc';
import { ISignal } from '@lumino/signaling';

export interface IWorkflowDoc extends YDocument<IWorkflowDocChange> {
name: string;

getName(): string | undefined;
setName(name: string): void;

nameChanged: ISignal<IWorkflowDoc, StringChange>;
}

export interface IWorkflowDocChange extends DocumentChange {
nameChange?: StringChange;
stateChange?: StateChange<any>[];
}

export type StringChange = {
oldValue?: string;
newValue?: string;
};
11 changes: 11 additions & 0 deletions src/workflows/schema/workflow.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Workflow",
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "The name of the workflow."
}
}
}
54 changes: 54 additions & 0 deletions src/workflows/workflowDoc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { YDocument } from '@jupyter/ydoc';
import * as Y from 'yjs';
import { IWorkflowDoc, IWorkflowDocChange, StringChange } from './interfaces';
import { ISignal, Signal } from '@lumino/signaling';

export class WorkflowDoc
extends YDocument<IWorkflowDocChange>
implements IWorkflowDoc
{
constructor() {
super();

this._name = this.ydoc.getText('name');
this._previousName = this._name.toString();
this._name.observe(this._nameObserver);
}

private _nameObserver = (event: Y.YTextEvent): void => {
const oldValue = this._previousName;
const newValue = this._name.toString();

this._previousName = newValue;

this._nameChanged.emit({ oldValue, newValue });
};

get nameChanged(): ISignal<IWorkflowDoc, StringChange> {
return this._nameChanged;
}

get name(): string {
return this._name.toString();
}

get version(): string {
return '0.0.1';
}

getName(): string | undefined {
return this.name;
}

setName(name: string): void {
const currentLength = this._name.length;
if (currentLength > 0) {
this._name.delete(0, currentLength);
}
this._name.insert(0, name);
}

private _name: Y.Text;
private _previousName: string;
private _nameChanged = new Signal<IWorkflowDoc, StringChange>(this);
}
170 changes: 170 additions & 0 deletions src/workflows/workflowModel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import { DocumentRegistry } from '@jupyterlab/docregistry';
import { PartialJSONObject } from '@lumino/coreutils';
import { ISignal, Signal } from '@lumino/signaling';
import { IWorkflowDoc } from './interfaces';
import { WorkflowDoc } from './workflowDoc';
import { Contents } from '@jupyterlab/services';

export interface IWorkflowModel extends DocumentRegistry.IModel {
sharedModel: IWorkflowDoc;
}

export class WorkflowModel implements IWorkflowModel {
constructor(options: DocumentRegistry.IModelOptions<IWorkflowDoc>) {
this._sharedModel = options.sharedModel ?? this.createSharedModel();
this._isDisposed = false;
this._dirty = false;
this._readOnly = false;

// Listen to changes on the shared model
this._sharedModel.nameChanged.connect(this._onNameChanged, this);
}

/**
* Create a default shared model if one is not provided.
*/
protected createSharedModel(): IWorkflowDoc {
return new WorkflowDoc();
}

get sharedModel(): IWorkflowDoc {
return this._sharedModel;
}

get isDisposed(): boolean {
return this._isDisposed;
}

get dirty(): boolean {
return this._dirty;
}

set dirty(value: boolean) {
this._dirty = value;
}

get readOnly(): boolean {
return this._readOnly;
}

set readOnly(value: boolean) {
this._readOnly = value;
}

get contentChanged(): ISignal<this, void> {
return this._contentChanged;
}

get stateChanged(): ISignal<this, any> {
return this._stateChanged;
}

/**
* Convert the model to string (JSON in this case).
* We only have a `name` field, so just return a JSON string with that.
*/
toString(): string {
const data = { name: this.sharedModel.getName() };
return JSON.stringify(data, null, 2);
}

/**
* Load from a string. Assume it’s JSON with a `name` field.
*/
fromString(data: string): void {
const jsonData = JSON.parse(data);
if (jsonData.name && typeof jsonData.name === 'string') {
this.sharedModel.transact(() => {
this.sharedModel.setName(jsonData.name);
});
this.dirty = true;
this._contentChanged.emit(void 0);
}
}

toJSON(): PartialJSONObject {
return JSON.parse(this.toString());
}

fromJSON(data: PartialJSONObject): void {
if (data.name && typeof data.name === 'string') {
this.sharedModel.transact(() => {
this.sharedModel.setName(data.name as string);
});
this.dirty = true;
this._contentChanged.emit(void 0);
}
}

initialize(): void {
// No initialization needed for this simple example
}

dispose(): void {
if (this.isDisposed) {
return;
}
this._isDisposed = true;

// Disconnect signals
this._sharedModel.nameChanged.disconnect(this._onNameChanged, this);

Signal.clearData(this);
}

private _onNameChanged(): void {
this.dirty = true;
this._contentChanged.emit(void 0);
}

private _sharedModel: IWorkflowDoc;
private _dirty: boolean;
private _readOnly: boolean;
private _isDisposed: boolean;

private _contentChanged = new Signal<this, void>(this);
private _stateChanged = new Signal<this, any>(this);

readonly defaultKernelName = '';
readonly defaultKernelLanguage = '';
}

export class WorkflowModelFactory
implements DocumentRegistry.IModelFactory<WorkflowModel>
{
//TODO: set conditionally
readonly collaborative = true;

get name(): string {
return 'workflow-model-factory';
}

get contentType(): Contents.ContentType {
return 'file';
}

get fileFormat(): Contents.FileFormat {
return 'text';
}

get isDisposed(): boolean {
return this._disposed;
}

dispose(): void {
this._disposed = true;
}

preferredLanguage(path: string): string {
return '';
}

createNew(
options: DocumentRegistry.IModelOptions<IWorkflowDoc>
): WorkflowModel {
const model = new WorkflowModel(options);
return model;
}

private _disposed = false;
}
Loading
Loading