import * as actions from "@nteract/actions" ; import { Notebook , stringifyNotebook , toJS } from "@nteract/commutable" ; import { defineConfigOption } from "@nteract/mythic-configuration" ; import { sendNotification } from "@nteract/mythic-notifications" ; import * as selectors from "@nteract/selectors" ; import { AppState , ContentRef , createKernelRef , DirectoryContentRecordProps , DummyContentRecordProps , FileContentRecordProps , IContent , IContentProvider , JupyterHostRecord , NotebookContentRecordProps , ServerConfig } from "@nteract/types" ; import FileSaver from "file-saver" ; import { RecordOf } from "immutable" ; import * as path from "path" ; import { Action } from "redux" ; import { ofType , StateObservable } from "redux-observable" ; import { EMPTY , from , interval , Observable , of } from "rxjs" ; import { AjaxResponse } from "rxjs/ajax" ; import { catchError , filter , map , mergeMap , switchMap , tap } from "rxjs/operators" ; import urljoin from "url-join" ; export function updateContentEpic ( action$ , state$ , dependencies ) { return action$ . pipe ( ofType ( actions . CHANGE_CONTENT_NAME ) , switchMap ( action => { const state = state$ . value ; const { filepath , prevFilePath } = action . payload ; const host = selectors . currentHost ( state ) as JupyterHostRecord ; const serverConfig = selectors . serverConfig ( host ) ; return dependencies . contentProvider . update ( serverConfig , prevFilePath , { path : filepath . slice ( 1 ) } ) . pipe ( tap ( xhr => { if ( xhr . status !== 200 ) { throw new Error ( xhr . response ) ; } } ) , map ( ( ) => { window . history . replaceState ( { } , filepath , urljoin ( host . basePath , `/nteract/edit${ filepath }` ) ) ; return actions . changeContentNameFulfilled ( { contentRef : action . payload . contentRef , filepath : action . payload . filepath , prevFilePath } ) ; } ) , catchError ( ( xhrError ) => of ( actions . changeContentNameFailed ( { basepath : host . basePath , filepath : action . payload . filepath , prevFilePath , error : xhrError , contentRef : action . payload . contentRef } ) ) ) ) ; } ) ) ; } export function fetchContentEpic ( action$ , state$ , dependencies ) { return action$ . pipe ( ofType ( actions . FETCH_CONTENT ) , switchMap ( action => { const state = state$ . value ; const host = selectors . currentHost ( state ) as JupyterHostRecord ; const serverConfig = selectors . serverConfig ( host ) ; return dependencies . contentProvider . get ( serverConfig , ( action as actions . FetchContent ) . payload . filepath , ( action as actions . FetchContent ) . payload . params ) . pipe ( tap ( xhr => { if ( xhr . status !== 200 ) { throw new Error ( xhr . response . toString ( ) ) ; } } ) , 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 } ) ; } ) , catchError ( ( xhrError ) => of ( actions . fetchContentFailed ( { filepath : action . payload . filepath , error : xhrError , kernelRef : action . payload . kernelRef , contentRef : action . payload . contentRef } ) ) ) ) ; } ) ) ; } export function downloadString ( fileContents , filepath , contentType ) { const filename = filepath . split ( "/" ) . pop ( ) ; const blob = new Blob ( [ fileContents ] , { type : contentType } ) ; FileSaver . saveAs ( blob , filename ) ; } const { selector : autoSaveInterval } = defineConfigOption ( { key : "autoSaveInterval" , label : "Auto-save interval" , defaultValue : 120000 , } ) ; export function autoSaveCurrentContentEpic ( action$ , state$ ) { return state$ . pipe ( map ( state => autoSaveInterval ( state ) ) , switchMap ( time => interval ( time ) ) , mergeMap ( ( ) => { const state = state$ . value ; return from ( selectors . contentByRef ( state ) . filter ( content => ( content . type === "file" || content . type === "notebook" ) && content . filepath !== "" ) . keys ( ) ) ; } ) , filter ( ( contentRef ) => { const model = selectors . model ( state$ . value , { contentRef } ) ; if ( model && model . type === "notebook" ) { return selectors . notebook . isDirty ( model ) ; } return false ; } ) , map ( ( contentRef ) => actions . save ( { contentRef } ) ) ) ; } function serializeContent ( state , content ) { let serializedData ; let saveModel = { } ; if ( content . type === "notebook" ) { const appVersion = selectors . appVersion ( state ) ; serializedData = 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 } ; } export function saveContentEpic ( action$ , state$ , dependencies ) { return action$ . pipe ( ofType ( actions . SAVE , actions . DOWNLOAD_CONTENT ) , mergeMap ( ( action ) : Observable < actions . DownloadContentFailed | actions . DownloadContentFulfilled | actions . SaveFailed | actions . SaveFulfilled > | Observable < never > => { 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 } ; if ( action . type === actions . DOWNLOAD_CONTENT ) { return of ( actions . downloadContentFailed ( errorPayload ) ) ; } return of ( actions . saveFailed ( errorPayload ) ) ; } if ( content . type === "directory" ) { return 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 of ( actions . saveFailed ( { error : new Error ( "No serialized model created for this content." ) , contentRef : action . payload . contentRef } ) ) ; } switch ( action . type ) { case actions . DOWNLOAD_CONTENT : { if ( content . type === "notebook" && typeof serializedData === "object" ) { downloadString ( stringifyNotebook ( serializedData ) , filepath || "notebook.ipynb" , "application/json" ) ; } else if ( content . type === "file" && typeof serializedData === "string" ) { downloadString ( serializedData , filepath , content . mimetype || "application/octet-stream" ) ; } else { return EMPTY ; } return of ( actions . downloadContentFulfilled ( { contentRef : action . payload . contentRef } ) ) ; } case actions . SAVE : { const host = selectors . currentHost ( state ) as JupyterHostRecord ; const serverConfig = selectors . serverConfig ( host ) ; return dependencies . contentProvider . save ( serverConfig , filepath , saveModel ) . pipe ( mergeMap ( ( saveXhr ) => { if ( saveXhr . response . errno ) { return of ( actions . saveFailed ( { contentRef : action . payload . contentRef , error : saveXhr . response } ) ) ; } return of ( actions . saveFulfilled ( { contentRef : action . payload . contentRef , model : saveXhr . response } ) ) ; } ) , catchError ( ( error ) => of ( actions . saveFailed ( { error , contentRef : action . payload . contentRef } ) ) ) ) ; } default : return EMPTY ; } } ) ) ; } export function saveAsContentEpic ( action$ , state$ , dependencies ) { return action$ . pipe ( ofType ( actions . SAVE_AS ) , mergeMap ( ( action ) : Observable < actions . SaveAsFailed | actions . SaveAsFulfilled > | Observable < never > => { 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 of ( actions . saveAsFailed ( errorPayload ) ) ; } if ( content . type === "directory" ) { return 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 of ( actions . saveAsFailed ( { error : new Error ( "No serialized model created for this content." ) , contentRef : action . payload . contentRef } ) ) ; } const host = selectors . currentHost ( state ) as JupyterHostRecord ; 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 ( 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 as any ) . store ) { ( window as any ) . store . dispatch ( actions . launchKernelByName ( { kernelSpecName : kernel . kernelSpecName , cwd , kernelRef : createKernelRef ( ) , selectNextKernel : true , contentRef } ) ) ; } } } } ) ) ; } } } return dependencies . contentProvider . save ( serverConfig , filepath , saveModel ) . pipe ( mergeMap ( ( xhr ) => { return of ( actions . changeFilename ( { contentRef : action . payload . contentRef , filepath } ) , actions . saveAsFulfilled ( { contentRef : action . payload . contentRef , model : xhr . response } ) , ... alertKernelChanged ) ; } ) , catchError ( ( error ) => of ( actions . saveAsFailed ( { error , contentRef : action . payload . contentRef } ) ) ) ) ; } ) ) ; } export function closeNotebookEpic ( action$ , state$ ) { return action$ . pipe ( ofType ( actions . CLOSE_NOTEBOOK ) , mergeMap ( ( action ) : Observable < actions . DisposeContent | actions . KillKernelAction > => { const state = state$ . value ; const contentRef = action . payload . contentRef ; const kernelRef = selectors . kernelRefByContentRef ( state , { contentRef } ) ; return of ( actions . disposeContent ( { contentRef } ) , actions . killKernel ( { kernelRef , restarting : false , dispose : true } ) ) ; } ) ) ; }