Skip to content

Commit

Permalink
Merge pull request #7 from PainterQubits/#5-add-warning-if-another-us…
Browse files Browse the repository at this point in the history
…er-has-the-file-open

#5 Add warning if another user has the file open
  • Loading branch information
alexhad6 authored Oct 25, 2023
2 parents 24dafc6 + 45065d6 commit 0a7d95c
Show file tree
Hide file tree
Showing 7 changed files with 497 additions and 17 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added

- JupyterLab plugin to display a warning dialog when opening a file that another user has
open.

## [0.2.0] (Oct 20 2023)

### Added
Expand Down
8 changes: 6 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"webpackConfig": "webpack.config.js"
},
"scripts": {
"dev": "yarn build && hatch run jupyter labextension develop --overwrite . && run-p watch:src watch:labextension jupyterlab",
"dev": "yarn build && hatch run jupyterlab:develop && run-p watch:src watch:labextension jupyterlab",
"build": "run-s clean cp:icons && tsc && hatch run jupyter labextension build .",
"preview": "yarn build && hatch env remove jupyterlab && yarn jupyterlab",
"clean": "rm -rf lib tsconfig.tsbuildinfo labextension",
Expand All @@ -23,7 +23,10 @@
"jupyterlab": "hatch run jupyterlab:start"
},
"dependencies": {
"@jupyter/collaboration": "^1.2.0",
"@jupyterlab/application": "^4.0.7",
"@jupyterlab/apputils": "^4.1.7",
"@jupyterlab/docmanager": "^4.0.7",
"@jupyterlab/filebrowser": "^4.0.7",
"@jupyterlab/launcher": "^4.0.7",
"@jupyterlab/notebook": "^4.0.7",
Expand All @@ -33,7 +36,8 @@
"eslint-import-resolver-typescript": "^3.6.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-pdf": "^7.5.1"
"react-pdf": "^7.5.1",
"y-protocols": "^1.0.6"
},
"devDependencies": {
"@jupyterlab/builder": "^4.0.7",
Expand Down
4 changes: 4 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ exclude = ["**/*"]
"labextension" = "share/jupyter/labextensions/datalogger-jupyterlab"
"install.json" = "share/jupyter/labextensions/datalogger-jupyterlab/install.json"

# For building and linting
[tool.hatch.envs.default]
dependencies = [
"jupyterlab>=4.0.7,<5",
Expand All @@ -49,9 +50,11 @@ dependencies = [
"black>=23.10.0,<24",
]

# For running JupterLab in development
[tool.hatch.envs.jupyterlab]
dependencies = [
"jupyterlab>=4.0.7,<5",
"jupyter-collaboration>=1.2.0,<2",
"datalogger>=0.2.0",
"numpy>=1.26.1,<2",
"xarray>=2023.10.1",
Expand All @@ -63,6 +66,7 @@ PIP_EXTRA_INDEX_URL = "https://painterqubits.github.io/datalogger/releases"

[tool.hatch.envs.jupyterlab.scripts]
start = "jupyter lab"
develop = "jupyter labextension develop --overwrite ."

[build-system]
requires = ["hatchling"]
Expand Down
8 changes: 7 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,14 @@ import {
netcdfFileTypePlugin,
dataloggerLoadCodePlugin,
pdfPreviewPlugin,
openWarningPlugin,
} from "@/plugins";

applyModifications();

export default [netcdfFileTypePlugin, dataloggerLoadCodePlugin, pdfPreviewPlugin];
export default [
netcdfFileTypePlugin,
dataloggerLoadCodePlugin,
pdfPreviewPlugin,
openWarningPlugin,
];
1 change: 1 addition & 0 deletions src/plugins/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { default as netcdfFileTypePlugin } from "./netcdfFileTypePlugin";
export { default as dataloggerLoadCodePlugin } from "./dataloggerLoadCodePlugin";
export { default as pdfPreviewPlugin } from "./pdfPreviewPlugin";
export { default as openWarningPlugin } from "./openWarningPlugin";
128 changes: 128 additions & 0 deletions src/plugins/openWarningPlugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import path from "path";
import { Awareness } from "y-protocols/awareness";
import { JupyterFrontEnd, JupyterFrontEndPlugin } from "@jupyterlab/application";
import { IDocumentManager } from "@jupyterlab/docmanager";
import { User } from "@jupyterlab/services";
import { IGlobalAwareness } from "@jupyter/collaboration";
import { showDialog, Dialog } from "@jupyterlab/apputils";

/** State within the awareness object. */
type AwarenessState = {
user?: User.IIdentity;
current?: string | null;
timestamp?: number;
};

/**
* Plugin that displays a warning dialog when opening a file that another user has open.
*/
const openWarningPlugin: JupyterFrontEndPlugin<void> = {
id: "datalogger-jupyterlab:open-warning",
description:
"Displays a warning dialog when opening a file that another user has open.",
autoStart: true,
requires: [IDocumentManager, IGlobalAwareness],
activate: (
_app: JupyterFrontEnd,
docManager: IDocumentManager,
awareness: Awareness,
) => {
/**
* Saved string corresponding to the current document, or null if not on a document.
*/
let savedCurrent: string | null = null;

/**
* Saved timestamp for when the current document was opened. If another user has the
* file open, this will be compared against their timestamp to determine whether to
* display a warning dialog. If null, then no warning dialog will be displayed.
*/
let savedTimestamp: number | null = null;

/**
* Whether there is currently a warning dialog open, to prevent additional dialogs
* from being opened simultaneously.
*/
let dialogIsOpen = false;

awareness.on("change", async () => {
const states: Map<number, AwarenessState> = awareness.getStates();
const myClientID = awareness.clientID;
const myCurrent = states.get(myClientID)?.current ?? null;

// If the current document is different than the saved document, update the saved
// document and timestamp, and update the timestamp in the awareness; otherwise,
// display a warning dialog if another user has the same document open with an older
// timestamp
if (myCurrent !== savedCurrent) {
savedCurrent = myCurrent;
savedTimestamp = Date.now();
awareness.setLocalStateField("timestamp", savedTimestamp);
} else if (!dialogIsOpen && savedCurrent !== null && savedTimestamp !== null) {
// Find the oldest state among other users who are on the same document and have
// an older timestamp. (The purpose of finding the user with the *oldest*
// timestamp, not just *an* older timestamp, is to display the name of the user
// with the highest priority in the dialog warning message.)
let oldestState: {
clientID: number;
user?: User.IIdentity;
timestamp: number;
} | null = null;
for (const [clientID, { user, current, timestamp }] of states) {
if (
current === savedCurrent &&
timestamp !== undefined &&
clientID !== myClientID &&
savedTimestamp >= timestamp &&
(oldestState === null || oldestState.timestamp > timestamp)
) {
oldestState = {
clientID,
user,
timestamp,
};
}
}

// If another user on the same document has an older timestamp, display a dialog
// warning message
if (oldestState !== null) {
const { clientID, user } = oldestState;
const pathComponents = savedCurrent.split(":");
const filename = path.basename(pathComponents[2]);

// Prevent other dialogs from being opened simultaneously
dialogIsOpen = true;

// Display the warning dialog
const {
button: { accept },
} = await showDialog({
title: "File already open",
body:
`${user?.name ?? clientID} already has "${filename}" open. Would you like` +
" to close this file to avoid conflicts?",
buttons: [
Dialog.cancelButton({ label: "Keep Open" }),
Dialog.okButton({ label: "Close" }),
],
});

// Setting the saved timestamp to null prevents additional dialogs for being
// shown until the user switches away and back to this file
savedTimestamp = null;

// If "Close" was selected, attempt to close the file
if (accept) {
await docManager.closeFile(pathComponents.slice(1).join(":"));
}

// Allow future dialogs to be opened
dialogIsOpen = false;
}
}
});
},
};

export default openWarningPlugin;
Loading

0 comments on commit 0a7d95c

Please sign in to comment.