Skip to content

Commit

Permalink
collaborative timeline slider added in the status bar
Browse files Browse the repository at this point in the history
dependency conflicts

fixed timestamp order

fork handler added

connect fork

read only notebook connected (fork)

fork document in frontend

fork in the frontend + apply updates

added fork indication to the page

added fork indication to the page

distinction between fork and original document added to slider

adjustement in fork handler

file

add css slider

context menu option for opening file (fork) + timeline added

notebook condition added

file menu option to open timeline for notebook

alternative: add file menu option to open right widget

added timeline slider to status bar

slider added to status bar

status bar timeline slider with logic to switch between notebooks added

error handling in the backend handler

clean console

add isActive to the registerStatusItem

fix: internal server error fixed in updateTimelineforNotebook function

fork cnd added to timeline status bar plugin

undo manager refactoring

fork logic for 1 notebook.

fork logic implemented for multiple notebooks opened at the same time

ydoc.share

fork works with multiple notebooks + freezing notebook when sliding through timeline

notebook id

testing cad

undo manager tests

clearDoc method

remove is timelineFork open

rebase

build error resolved

using undo manager in the backend

undo/redo steps added

freeze document

updated to the new version of pycrdt, fixed problem with undostack

rebase

restore version btn

undo manager agnostic for different type of documents

restoring version

restoring version

restore btn : fix style

delete unused files

delete unused files

jupytercad

jupyter cad

jupytercad integration + rebase main

conflicts

conflicts

fixed console error: empty response.

icon visible only when data is not null

moving fetch timeline in slider component: on click of the history button

get format & content type from query params

get format & content type from query params: fix updating contenttype and format when switching between documents

remove sharedmodel from update content

support for documents inside folders/subfolders.

clean drive.ts

move test files in folder

delete unused dependency

return comments deleted by accident

fixes in jupyter-server-ydoc

delete test documents

add test-folder to gitignore

styling restore button

styling restore button

pre commit hooks

fixed pre commit issues

fixed pre commit issues

add license header to new files

pre commit hooks

python test: added jupytercad_core to CI/CD workflow

python test debug

python test debug

collaborative timeline slider added in the status bar

dependency conflicts

fixed timestamp order

fork handler added

connect fork

read only notebook connected (fork)

fork document in frontend

fork in the frontend + apply updates

added fork indication to the page

added fork indication to the page

distinction between fork and original document added to slider

adjustement in fork handler

file

add css slider

context menu option for opening file (fork) + timeline added

notebook condition added

file menu option to open timeline for notebook

alternative: add file menu option to open right widget

added timeline slider to status bar

slider added to status bar

status bar timeline slider with logic to switch between notebooks added

error handling in the backend handler

clean console

add isActive to the registerStatusItem

fix: internal server error fixed in updateTimelineforNotebook function

fork cnd added to timeline status bar plugin

undo manager refactoring

fork logic for 1 notebook.

fork logic implemented for multiple notebooks opened at the same time

ydoc.share

fork works with multiple notebooks + freezing notebook when sliding through timeline

notebook id

testing cad

undo manager tests

clearDoc method

remove is timelineFork open

rebase

build error resolved

using undo manager in the backend

undo/redo steps added

freeze document

updated to the new version of pycrdt, fixed problem with undostack

rebase

restore version btn

undo manager agnostic for different type of documents

restoring version

restoring version

restore btn : fix style

delete unused files

delete unused files

jupytercad

jupyter cad

jupytercad integration + rebase main

conflicts

conflicts

fixed console error: empty response.

icon visible only when data is not null

moving fetch timeline in slider component: on click of the history button

get format & content type from query params

get format & content type from query params: fix updating contenttype and format when switching between documents

remove sharedmodel from update content

support for documents inside folders/subfolders.

clean drive.ts

move test files in folder

delete unused dependency

return comments deleted by accident

fixes in jupyter-server-ydoc

delete test documents

add test-folder to gitignore

styling restore button

styling restore button

pre commit hooks

fixed pre commit issues

python test debug: test.yaml

python test debug: test.yaml

python test debug: test.yaml

changed order of dependencies in test.yml

removed jupytercad to test dependencies version

removed jupytercad to test dependencies version

pre commit

changed the way document types are imported in the backend

fixed yarn.lock after rebase
  • Loading branch information
Meriem-BenIsmail committed Aug 23, 2024
1 parent 1b74c43 commit 05df889
Show file tree
Hide file tree
Showing 15 changed files with 2,387 additions and 1,332 deletions.
71 changes: 69 additions & 2 deletions packages/docprovider-extension/src/filebrowser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,14 @@ import {
} from '@jupyterlab/application';
import { Dialog, showDialog } from '@jupyterlab/apputils';
import { IDocumentWidget } from '@jupyterlab/docregistry';
import { Widget } from '@lumino/widgets';
import {
FileBrowser,
IDefaultFileBrowser,
IFileBrowserFactory
} from '@jupyterlab/filebrowser';
import { IStatusBar } from '@jupyterlab/statusbar';

import { IEditorTracker } from '@jupyterlab/fileeditor';
import { ILogger, ILoggerRegistry } from '@jupyterlab/logconsole';
import { INotebookTracker } from '@jupyterlab/notebook';
Expand Down Expand Up @@ -91,7 +94,7 @@ export const ynotebook: JupyterFrontEndPlugin<void> = {
optional: [ISettingRegistry],
activate: (
app: JupyterFrontEnd,
drive: ICollaborativeDrive,
drive: YDrive,
settingRegistry: ISettingRegistry | null
): void => {
let disableDocumentWideUndoRedo = true;
Expand Down Expand Up @@ -127,6 +130,69 @@ export const ynotebook: JupyterFrontEndPlugin<void> = {
);
}
};
/**
* A plugin to add a timeline slider status item to the status bar.
*/
export const statusBarTimeline: JupyterFrontEndPlugin<void> = {
id: '@jupyter/docprovider-extension:statusBarTimeline',
description: 'Plugin to add a timeline slider to the status bar',
autoStart: true,
requires: [IStatusBar],
optional: [ICollaborativeDrive],
activate: async (
app: JupyterFrontEnd,
statusBar: IStatusBar,
drive: YDrive | null
): Promise<void> => {
try {
if (!drive) {
console.warn('Collaborative drive not available');
return;
}

let sliderItem: Widget | null = null;
const updateTimelineForDocument = async (document: any) => {
if (document && document.context) {
const { context } = document;
const documentPath = context.path;
const sharedModel = context.model.sharedModel;
document.node.id = sharedModel.ydoc.guid;
await drive.updateTimelineForNotebook(documentPath);
}
};

if (app.shell.currentChanged) {
app.shell.currentChanged.connect(async (_, args) => {
const currentWidget = args.newValue;

if (currentWidget && 'context' in currentWidget) {
await updateTimelineForDocument(currentWidget);
}
});
}
if (statusBar) {
if (!sliderItem) {
sliderItem = new Widget();
sliderItem.addClass('jp-StatusBar-GroupItem');
sliderItem.addClass('jp-mod-highlighted');

sliderItem.id = 'slider-status-bar';
statusBar.registerStatusItem('slider-status-bar', {
item: sliderItem,
align: 'left',
rank: 4,
isActive: () => {
const currentWidget = app.shell.currentWidget;
return !!currentWidget && 'context' in currentWidget;
}
});
}
}
} catch (error) {
console.error('Failed to activate statusBarTimeline plugin:', error);
}
}
};

/**
* The default file browser factory provider.
Expand All @@ -144,7 +210,7 @@ export const defaultFileBrowser: JupyterFrontEndPlugin<IDefaultFileBrowser> = {
],
activate: async (
app: JupyterFrontEnd,
drive: ICollaborativeDrive,
drive: YDrive,
fileBrowserFactory: IFileBrowserFactory,
router: IRouter | null,
tree: JupyterFrontEnd.ITreeResolver | null,
Expand Down Expand Up @@ -292,6 +358,7 @@ namespace Private {
router.routed.disconnect(listener);

const paths = await tree?.paths;

if (paths?.file || paths?.browser) {
// Restore the model without populating it.
await browser.model.restore(browser.id, false);
Expand Down
6 changes: 4 additions & 2 deletions packages/docprovider-extension/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ import {
yfile,
ynotebook,
defaultFileBrowser,
logger
logger,
statusBarTimeline
} from './filebrowser';
import { notebookCellExecutor } from './executor';

Expand All @@ -25,7 +26,8 @@ const plugins: JupyterFrontEndPlugin<any>[] = [
ynotebook,
defaultFileBrowser,
logger,
notebookCellExecutor
notebookCellExecutor,
statusBarTimeline
];

export default plugins;
61 changes: 61 additions & 0 deletions packages/docprovider/src/TimelineSlider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/* -----------------------------------------------------------------------------
| Copyright (c) Jupyter Development Team.
| Distributed under the terms of the Modified BSD License.
|----------------------------------------------------------------------------*/

import { ReactWidget } from '@jupyterlab/apputils';
import { TimelineSliderComponent } from './component';
import * as React from 'react';
import { WebSocketProvider } from './yprovider';

export class TimelineWidget extends ReactWidget {
private apiURL: string;
private provider: WebSocketProvider;
private contentType: string;
private format: string;

constructor(
apiURL: string,
provider: WebSocketProvider,
contentType: string,
format: string
) {
super();
this.apiURL = apiURL;
this.provider = provider;
this.contentType = contentType;
this.format = format;
this.addClass('timeline-slider-wrapper');
}

render(): JSX.Element {
return (
<TimelineSliderComponent
key={this.apiURL}
apiURL={this.apiURL}
provider={this.provider}
contentType={this.contentType}
format={this.format}
/>
);
}
updateContent(apiURL: string, provider: WebSocketProvider): void {
this.apiURL = apiURL;
this.provider = provider;
this.contentType = this.provider.contentType;
this.format = this.provider.format;

this.update();
}
extractFilenameFromURL(url: string): string {
try {
const parsedURL = new URL(url);
const pathname = parsedURL.pathname;
const segments = pathname.split('/');
return segments[segments.length - 1];
} catch (error) {
console.error('Invalid URL:', error);
return '';
}
}
}
187 changes: 187 additions & 0 deletions packages/docprovider/src/component.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
/* -----------------------------------------------------------------------------
| Copyright (c) Jupyter Development Team.
| Distributed under the terms of the Modified BSD License.
|----------------------------------------------------------------------------*/

import React, { useState, useRef } from 'react';
import '../style/slider.css';
import { WebSocketProvider } from './yprovider';
import { requestDocFork, requestDocumentTimeline } from './requests';
import { historyIcon } from '@jupyterlab/ui-components';
import { Notification } from '@jupyterlab/apputils';

type Props = {
apiURL: string;
provider: WebSocketProvider;
contentType: string;
format: string;
};

export const TimelineSliderComponent: React.FC<Props> = ({
apiURL,
provider,
contentType,
format
}) => {
const [data, setData] = useState({ roomId: '', timestamps: [] });
const [currentTimestampIndex, setCurrentTimestampIndex] = useState(
data.timestamps.length - 1
);
const [session, setSession]: any = useState();
const [forkRoomID, setForkRoomID]: any = useState();
const [toggle, setToggle] = useState(false);
const [isBtn, setIsBtn] = useState(false);

const isFirstChange = useRef(true);

async function fetchTimeline(notebookPath: string) {
try {
const response = await requestDocumentTimeline(
format,
contentType,
notebookPath
);

if (!response.ok) {
if (response.status === 404) {
throw new Error('Not found');
} else if (response.status === 503) {
throw new Error('WebSocket closed');
} else {
throw new Error(`Failed to fetch data: ${response.statusText}`);
}
}
const text = await response.text();
let data = { roomId: '', timestamps: [] };
if (text) {
data = JSON.parse(text);
setData(data);
setCurrentTimestampIndex(data.timestamps.length - 1);
}
setToggle(true);

return data;
} catch (error) {
console.error('Error fetching data:', error);
}
}
const handleClick = async () => {
const response = await requestDocFork(
`${session.format}:${session.type}:${session.fileId}`,
'undo',
'restore',
0
);
if (response.code === 200) {
Notification.success(response.status, { autoClose: 4000 });
} else {
Notification.error(response.status, { autoClose: 4000 });
}
};
const handleSliderChange = async (
event: React.ChangeEvent<HTMLInputElement>
) => {
const currentTimestamp = parseInt(event.target.value);
const steps = Math.abs(currentTimestamp - currentTimestampIndex);

try {
const action = determineAction(currentTimestamp);
setCurrentTimestampIndex(currentTimestamp);

// create fork when first using the slider
if (isFirstChange.current) {
setIsBtn(true);
isFirstChange.current = false;
const obj = await provider.connectToFork(action, 'original', steps);
setForkRoomID(obj.forkRoomId);
setSession(obj.session);
} else if (session && forkRoomID) {
await requestDocFork(
`${session.format}:${session.type}:${session.fileId}`,
action,
'fork',
steps
);
}
} catch (error: any) {
console.error('Error fetching or applying updates:', error);
}
};

function determineAction(currentTimestamp: number): 'undo' | 'redo' {
return currentTimestamp < currentTimestampIndex ? 'undo' : 'redo';
}

function extractFilenameFromURL(url: string): string {
try {
const parsedURL = new URL(url);
const pathname = parsedURL.pathname;
const segments = pathname.split('/');

return segments.slice(4 - segments.length).join('/');
} catch (error) {
console.error('Invalid URL:', error);
return '';
}
}

const formatTimestamp = (timestamp: number): string => {
const date = new Date(timestamp * 1000);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = String(date.getSeconds()).padStart(2, '0');

return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
};

return (
<div className="slider-container">
<div
onClick={() => {
fetchTimeline(extractFilenameFromURL(apiURL));
}}
className="jp-mod-highlighted"
title="Document Timeline"
>
<historyIcon.react marginRight="4px" />
</div>
{toggle && (
<div className="timestamp-display">
<input
type="range"
min={0}
max={data.timestamps.length - 1}
value={currentTimestampIndex}
onChange={handleSliderChange}
className="slider"
style={{ height: '4.5px' }}
/>
<div>
<strong>
{
extractFilenameFromURL(apiURL).split('/')[
extractFilenameFromURL(apiURL).split('/').length - 1
]
}{' '}
</strong>{' '}
</div>
{isBtn && (
<div className="restore-btn">
<button
onClick={handleClick}
className="jp-ToolbarButtonComponent restore-btn"
style={{ background: '#1976d2' }}
>
Restore version{' '}
{formatTimestamp(data.timestamps[currentTimestampIndex])}
</button>
</div>
)}
</div>
)}
</div>
);
};
1 change: 1 addition & 0 deletions packages/docprovider/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ export * from './requests';
export * from './ydrive';
export * from './yprovider';
export * from './tokens';
export * from './TimelineSlider';
Loading

0 comments on commit 05df889

Please sign in to comment.