UNPKG

@nteract/epics

Version:
333 lines 15.3 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.closeNotebookEpic = exports.saveAsContentEpic = exports.saveContentEpic = exports.autoSaveCurrentContentEpic = exports.downloadString = exports.fetchContentEpic = exports.updateContentEpic = void 0; const actions = __importStar(require("@nteract/actions")); const commutable_1 = require("@nteract/commutable"); const mythic_configuration_1 = require("@nteract/mythic-configuration"); const mythic_notifications_1 = require("@nteract/mythic-notifications"); const selectors = __importStar(require("@nteract/selectors")); const types_1 = require("@nteract/types"); const file_saver_1 = __importDefault(require("file-saver")); const path = __importStar(require("path")); const redux_observable_1 = require("redux-observable"); const rxjs_1 = require("rxjs"); const operators_1 = require("rxjs/operators"); const url_join_1 = __importDefault(require("url-join")); function updateContentEpic(action$, state$, dependencies) { return action$.pipe(redux_observable_1.ofType(actions.CHANGE_CONTENT_NAME), operators_1.switchMap(action => { const state = state$.value; const { filepath, prevFilePath } = action.payload; const host = selectors.currentHost(state); const serverConfig = selectors.serverConfig(host); return dependencies.contentProvider .update(serverConfig, prevFilePath, { path: filepath.slice(1) }) .pipe(operators_1.tap(xhr => { if (xhr.status !== 200) { throw new Error(xhr.response); } }), operators_1.map(() => { /* * Modifying the url's file name in the browser. * This effects back button behavior. * Is there a better way to accomplish this? */ window.history.replaceState({}, filepath, url_join_1.default(host.basePath, `/nteract/edit${filepath}`)); return actions.changeContentNameFulfilled({ contentRef: action.payload.contentRef, filepath: action.payload.filepath, prevFilePath }); }), operators_1.catchError((xhrError) => rxjs_1.of(actions.changeContentNameFailed({ basepath: host.basePath, filepath: action.payload.filepath, prevFilePath, error: xhrError, contentRef: action.payload.contentRef })))); })); } exports.updateContentEpic = updateContentEpic; function fetchContentEpic(action$, state$, dependencies) { return action$.pipe(redux_observable_1.ofType(actions.FETCH_CONTENT), operators_1.switchMap(action => { const state = state$.value; const host = selectors.currentHost(state); const serverConfig = selectors.serverConfig(host); return dependencies.contentProvider .get(serverConfig, action.payload.filepath, action.payload.params) .pipe(operators_1.tap(xhr => { if (xhr.status !== 200) { throw new Error(xhr.response.toString()); } }), operators_1.map(xhr => { if (typeof xhr.response === "string") { throw new Error(`Invalid API response: ${xhr.response}`); } return actions.fetchContentFulfilled({ filepath: action.payload.filepath, model: xhr.response, kernelRef: action.payload.kernelRef, contentRef: action.payload.contentRef }); }), operators_1.catchError((xhrError) => rxjs_1.of(actions.fetchContentFailed({ filepath: action.payload.filepath, error: xhrError, kernelRef: action.payload.kernelRef, contentRef: action.payload.contentRef })))); })); } exports.fetchContentEpic = fetchContentEpic; function downloadString(fileContents, filepath, contentType) { const filename = filepath.split("/").pop(); const blob = new Blob([fileContents], { type: contentType }); // NOTE: There is no callback for this, we have to rely on the browser // to do this well, so we assume it worked file_saver_1.default.saveAs(blob, filename); } exports.downloadString = downloadString; const { selector: autoSaveInterval } = mythic_configuration_1.defineConfigOption({ key: "autoSaveInterval", label: "Auto-save interval", defaultValue: 120000, }); function autoSaveCurrentContentEpic(action$, state$) { return state$.pipe(operators_1.map(state => autoSaveInterval(state)), operators_1.switchMap(time => rxjs_1.interval(time)), operators_1.mergeMap(() => { const state = state$.value; return rxjs_1.from(selectors .contentByRef(state) .filter( /* * Only save contents that are files or notebooks with * a filepath already set. */ content => (content.type === "file" || content.type === "notebook") && content.filepath !== "") .keys()); }), operators_1.filter((contentRef) => { const model = selectors.model(state$.value, { contentRef }); if (model && model.type === "notebook") { return selectors.notebook.isDirty(model); } return false; }), operators_1.map((contentRef) => actions.save({ contentRef }))); } exports.autoSaveCurrentContentEpic = autoSaveCurrentContentEpic; function serializeContent(state, content) { // This could be object for notebook, or string for files let serializedData; let saveModel = {}; if (content.type === "notebook") { const appVersion = selectors.appVersion(state); // contents API takes notebook as raw JSON whereas downloading takes // a string serializedData = commutable_1.toJS(content.model.notebook.setIn(["metadata", "nteract", "version"], appVersion)); saveModel = { content: serializedData, type: content.type }; } else if (content.type === "file") { serializedData = content.model.text; saveModel = { content: serializedData, type: content.type, format: "text" }; } else { return { saveModel: null, serializedData: null }; } return { saveModel, serializedData }; } function saveContentEpic(action$, state$, dependencies) { return action$.pipe(redux_observable_1.ofType(actions.SAVE, actions.DOWNLOAD_CONTENT), operators_1.mergeMap((action) => { const state = state$.value; const contentRef = action.payload.contentRef; const content = selectors.content(state, { contentRef }); // NOTE: This could save by having selectors for each model type // have toDisk() selectors // It will need to be cased off when we have more than one type // of content we actually save if (!content) { const errorPayload = { error: new Error("Content was not set."), contentRef: action.payload.contentRef }; if (action.type === actions.DOWNLOAD_CONTENT) { return rxjs_1.of(actions.downloadContentFailed(errorPayload)); } return rxjs_1.of(actions.saveFailed(errorPayload)); } if (content.type === "directory") { return rxjs_1.of(actions.saveFailed({ error: new Error("Cannot save directories."), contentRef: action.payload.contentRef })); } const filepath = content.filepath; const { serializedData, saveModel } = serializeContent(state, content); if (!saveModel || !serializedData) { return rxjs_1.of(actions.saveFailed({ error: new Error("No serialized model created for this content."), contentRef: action.payload.contentRef })); } switch (action.type) { case actions.DOWNLOAD_CONTENT: { // FIXME: Convert this to downloadString, so it works for // both files & notebooks if (content.type === "notebook" && typeof serializedData === "object") { downloadString(commutable_1.stringifyNotebook(serializedData), filepath || "notebook.ipynb", "application/json"); } else if (content.type === "file" && typeof serializedData === "string") { downloadString(serializedData, filepath, content.mimetype || "application/octet-stream"); } else { // This shouldn't happen, is here for safety return rxjs_1.EMPTY; } return rxjs_1.of(actions.downloadContentFulfilled({ contentRef: action.payload.contentRef })); } case actions.SAVE: { const host = selectors.currentHost(state); const serverConfig = selectors.serverConfig(host); return dependencies.contentProvider .save(serverConfig, filepath, saveModel) .pipe(operators_1.mergeMap((saveXhr) => { if (saveXhr.response.errno) { return rxjs_1.of(actions.saveFailed({ contentRef: action.payload.contentRef, error: saveXhr.response })); } return rxjs_1.of(actions.saveFulfilled({ contentRef: action.payload.contentRef, model: saveXhr.response })); }), operators_1.catchError((error) => rxjs_1.of(actions.saveFailed({ error, contentRef: action.payload.contentRef })))); } default: // NOTE: Our ofType should prevent reaching here, this // is here merely as safety return rxjs_1.EMPTY; } })); } exports.saveContentEpic = saveContentEpic; function saveAsContentEpic(action$, state$, dependencies) { return action$.pipe(redux_observable_1.ofType(actions.SAVE_AS), operators_1.mergeMap((action) => { const state = state$.value; const contentRef = action.payload.contentRef; const content = selectors.content(state, { contentRef }); if (!content) { const errorPayload = { error: new Error("Content was not set."), contentRef: action.payload.contentRef }; return rxjs_1.of(actions.saveAsFailed(errorPayload)); } if (content.type === "directory") { return rxjs_1.of(actions.saveAsFailed({ error: new Error("Cannot save directories."), contentRef: action.payload.contentRef })); } const filepath = action.payload.filepath; const { saveModel } = serializeContent(state, content); if (!saveModel) { return rxjs_1.of(actions.saveAsFailed({ error: new Error("No serialized model created for this content."), contentRef: action.payload.contentRef })); } const host = selectors.currentHost(state); const serverConfig = selectors.serverConfig(host); const kernelRef = selectors.kernelRefByContentRef(state, { contentRef }); const alertKernelChanged = []; if (kernelRef) { const kernel = selectors.kernel(state, { kernelRef }); if (kernel) { const cwd = path.dirname(path.resolve(filepath)); if (cwd !== kernel.cwd) { alertKernelChanged.push(mythic_notifications_1.sendNotification.create({ title: "Notebook folder changed", message: "The kernel executing your code thinks your notebook is still " + "in the old folder. Would you like to launch a new kernel in " + "the new folder?", level: "warning", action: { label: "Launch new kernel", callback() { if (window && window.store) { window.store.dispatch(actions.launchKernelByName({ kernelSpecName: kernel.kernelSpecName, cwd, kernelRef: types_1.createKernelRef(), selectNextKernel: true, contentRef })); } } } })); } } } return dependencies.contentProvider .save(serverConfig, filepath, saveModel) .pipe(operators_1.mergeMap((xhr) => { return rxjs_1.of(actions.changeFilename({ contentRef: action.payload.contentRef, filepath }), actions.saveAsFulfilled({ contentRef: action.payload.contentRef, model: xhr.response }), ...alertKernelChanged); }), operators_1.catchError((error) => rxjs_1.of(actions.saveAsFailed({ error, contentRef: action.payload.contentRef })))); })); } exports.saveAsContentEpic = saveAsContentEpic; function closeNotebookEpic(action$, state$) { return action$.pipe(redux_observable_1.ofType(actions.CLOSE_NOTEBOOK), operators_1.mergeMap((action) => { const state = state$.value; const contentRef = action.payload.contentRef; const kernelRef = selectors.kernelRefByContentRef(state, { contentRef }); return rxjs_1.of(actions.disposeContent({ contentRef }), actions.killKernel({ kernelRef, restarting: false, dispose: true })); })); } exports.closeNotebookEpic = closeNotebookEpic; //# sourceMappingURL=contents.js.map