Initial Move from Azure DevOps to GitHub

This commit is contained in:
Steve Faulkner
2020-05-25 21:30:55 -05:00
commit 36581fb6d9
986 changed files with 195242 additions and 0 deletions

View File

@@ -0,0 +1,13 @@
.notebookComponentContainer {
text-transform:none;
line-height:1.28581;
letter-spacing:0;
font-size:14px;
font-weight:400;
color:#182026;
height: 100%;
.hotKeys {
height: 100%;
}
}

View File

@@ -0,0 +1,25 @@
import { ContentRef } from "@nteract/core";
import * as React from "react";
import NotificationSystem, { System as ReactNotificationSystem } from "react-notification-system";
import { default as Contents } from "./contents";
export class NotebookComponent extends React.Component<{ contentRef: ContentRef }> {
notificationSystem!: ReactNotificationSystem;
shouldComponentUpdate(nextProps: { contentRef: ContentRef }): boolean {
return nextProps.contentRef !== this.props.contentRef;
}
public render(): JSX.Element {
return (
<div className="notebookComponentContainer">
<Contents contentRef={this.props.contentRef} />
<NotificationSystem
ref={(notificationSystem: ReactNotificationSystem) => {
this.notificationSystem = notificationSystem;
}}
/>
</div>
);
}
}

View File

@@ -0,0 +1,52 @@
import * as React from "react";
import { ReactAdapter } from "../../../Bindings/ReactBindingHandler";
import { NotebookClientV2 } from "../NotebookClientV2";
// Vendor modules
import { actions, createContentRef, createKernelRef, selectors } from "@nteract/core";
import VirtualCommandBarComponent from "./VirtualCommandBarComponent";
import { NotebookContentItem } from "../NotebookContentItem";
import { NotebookComponentBootstrapper } from "./NotebookComponentBootstrapper";
export interface NotebookComponentAdapterOptions {
contentItem: NotebookContentItem;
notebooksBasePath: string;
notebookClient: NotebookClientV2;
onUpdateKernelInfo: () => void;
}
export class NotebookComponentAdapter extends NotebookComponentBootstrapper implements ReactAdapter {
private onUpdateKernelInfo: () => void;
public parameters: any;
constructor(options: NotebookComponentAdapterOptions) {
super({
contentRef: selectors.contentRefByFilepath(options.notebookClient.getStore().getState(), {
filepath: options.contentItem.path
}),
notebookClient: options.notebookClient
});
this.onUpdateKernelInfo = options.onUpdateKernelInfo;
if (!this.contentRef) {
this.contentRef = createContentRef();
const kernelRef = createKernelRef();
// Request fetching notebook content
this.getStore().dispatch(
actions.fetchContent({
filepath: options.contentItem.path,
params: {},
kernelRef,
contentRef: this.contentRef
})
);
}
}
protected renderExtraComponent = (): JSX.Element => {
return <VirtualCommandBarComponent contentRef={this.contentRef} onRender={this.onUpdateKernelInfo} />;
};
}

View File

@@ -0,0 +1,314 @@
import * as React from "react";
import { NotebookComponent } from "./NotebookComponent";
import { NotebookClientV2 } from "../NotebookClientV2";
import { NotebookUtil } from "../NotebookUtil";
// Vendor modules
import {
actions,
AppState,
createKernelRef,
DocumentRecordProps,
ContentRef,
KernelRef,
NotebookContentRecord,
selectors
} from "@nteract/core";
import * as Immutable from "immutable";
import { Provider } from "react-redux";
import { CellType, CellId } from "@nteract/commutable";
import { Store, AnyAction } from "redux";
import "./NotebookComponent.less";
import "codemirror/addon/hint/show-hint.css";
import "codemirror/lib/codemirror.css";
import "@nteract/styles/editor-overrides.css";
import "@nteract/styles/global-variables.css";
import "react-table/react-table.css";
import * as CdbActions from "./actions";
import NteractUtil from "../NTeractUtil";
export interface NotebookComponentBootstrapperOptions {
notebookClient: NotebookClientV2;
contentRef: ContentRef;
}
export class NotebookComponentBootstrapper {
protected contentRef: ContentRef;
protected renderExtraComponent: () => JSX.Element;
private notebookClient: NotebookClientV2;
constructor(options: NotebookComponentBootstrapperOptions) {
this.notebookClient = options.notebookClient;
this.contentRef = options.contentRef;
}
protected static wrapModelIntoContent(name: string, path: string, content: any) {
return {
name,
path,
last_modified: new Date(),
created: "",
content,
format: "json",
mimetype: null as any,
size: 0,
writeable: false,
type: "notebook"
};
}
private renderDefaultNotebookComponent(props: any): JSX.Element {
return (
<>
{this.renderExtraComponent && this.renderExtraComponent()}
{React.createElement<{ contentRef: ContentRef }>(NotebookComponent, { contentRef: this.contentRef, ...props })}
</>
);
}
public setContent(name: string, content: any): void {
this.getStore().dispatch(
actions.fetchContentFulfilled({
filepath: undefined,
model: NotebookComponentBootstrapper.wrapModelIntoContent(name, undefined, content),
kernelRef: createKernelRef(),
contentRef: this.contentRef
})
);
}
/**
* We can overload the notebook renderer here
* @param renderer
* @props additional props
*/
public renderComponent(
renderer?: any, // TODO FIX THIS React.ComponentClass<{ contentRef: ContentRef; isReadOnly?: boolean }>,
props?: any
): JSX.Element {
return (
<Provider store={this.getStore()}>
{renderer
? React.createElement<{ contentRef: ContentRef }>(renderer, { contentRef: this.contentRef, ...props })
: this.renderDefaultNotebookComponent(props)}
</Provider>
);
}
/* Notebook operations. See nteract/packages/connected-components/src/notebook-menu/index.tsx */
public notebookSave(): void {
this.getStore().dispatch(
actions.save({
contentRef: this.contentRef
})
);
}
public notebookChangeKernel(kernelSpecName: string): void {
this.getStore().dispatch(
actions.changeKernelByName({
contentRef: this.contentRef,
kernelSpecName,
oldKernelRef: this.getCurrentKernelRef()
})
);
}
public notebookRunAndAdvance(): void {
this.getStore().dispatch(
CdbActions.executeFocusedCellAndFocusNext({
contentRef: this.contentRef
})
);
}
public notebookRunAll(): void {
this.getStore().dispatch(
actions.executeAllCells({
contentRef: this.contentRef
})
);
}
public notebookInterruptKernel(): void {
this.getStore().dispatch(
actions.interruptKernel({
kernelRef: this.getCurrentKernelRef()
})
);
}
public notebookKillKernel(): void {
this.getStore().dispatch(
actions.killKernel({
restarting: false,
kernelRef: this.getCurrentKernelRef()
})
);
}
public notebookRestartKernel(): void {
this.getStore().dispatch(
actions.restartKernel({
kernelRef: this.getCurrentKernelRef(),
contentRef: this.contentRef,
outputHandling: "None"
})
);
}
public notebookClearAllOutputs(): void {
this.getStore().dispatch(
actions.clearAllOutputs({
contentRef: this.contentRef
})
);
}
public notebookInsertBelow(): void {
this.getStore().dispatch(
actions.createCellBelow({
cellType: "code",
source: "",
contentRef: this.contentRef
})
);
}
public notebookChangeCellType(type: CellType): void {
const focusedCellId = this.getFocusedCellId();
if (!focusedCellId) {
console.error("No focused cell");
return;
}
this.getStore().dispatch(
actions.changeCellType({
id: focusedCellId,
contentRef: this.contentRef,
to: type
})
);
}
public notebokCopy(): void {
const focusedCellId = this.getFocusedCellId();
if (!focusedCellId) {
console.error("No focused cell");
return;
}
this.getStore().dispatch(
actions.copyCell({
id: focusedCellId,
contentRef: this.contentRef
})
);
}
public notebookCut(): void {
const focusedCellId = this.getFocusedCellId();
if (!focusedCellId) {
console.error("No focused cell");
return;
}
this.getStore().dispatch(
actions.cutCell({
id: focusedCellId,
contentRef: this.contentRef
})
);
}
public notebookPaste(): void {
this.getStore().dispatch(
actions.pasteCell({
contentRef: this.contentRef
})
);
}
public notebookShutdown(): void {
const store = this.getStore();
const kernelRef = this.getCurrentKernelRef();
if (kernelRef) {
store.dispatch(
actions.killKernel({
restarting: false,
kernelRef
})
);
}
store.dispatch(
CdbActions.closeNotebook({
contentRef: this.contentRef
})
);
}
public isContentDirty(): boolean {
const content = selectors.content(this.getStore().getState(), { contentRef: this.contentRef });
if (!content) {
console.log("No error");
return false;
}
return selectors.notebook.isDirty(content.model as Immutable.RecordOf<DocumentRecordProps>);
}
/**
* For display purposes, only return non-killed kernels
*/
public getCurrentKernelName(): string {
const currentKernel = selectors.kernel(this.getStore().getState(), { kernelRef: this.getCurrentKernelRef() });
return (currentKernel && currentKernel.status !== "killed" && currentKernel.kernelSpecName) || undefined;
}
// Returns the kernel name to select in the kernels dropdown
public getSelectedKernelName(): string {
const currentKernelName = this.getCurrentKernelName();
if (!currentKernelName) {
// if there's no live kernel, try to get the kernel name from the notebook metadata
const content = selectors.content(this.getStore().getState(), { contentRef: this.contentRef });
const notebook = content && (content as NotebookContentRecord).model.notebook;
if (!notebook) {
return undefined;
}
const { kernelSpecName } = NotebookUtil.extractNewKernel("", notebook);
return kernelSpecName || undefined;
}
return currentKernelName;
}
public getActiveCellTypeStr(): string {
const content = selectors.content(this.getStore().getState(), { contentRef: this.contentRef });
return NteractUtil.getCurrentCellType(content as NotebookContentRecord);
}
private getCurrentKernelRef(): KernelRef {
return selectors.kernelRefByContentRef(this.getStore().getState(), { contentRef: this.contentRef });
}
private getFocusedCellId(): CellId {
const content = selectors.content(this.getStore().getState(), { contentRef: this.contentRef });
if (!content) {
return undefined;
}
return selectors.notebook.cellFocused((content as NotebookContentRecord).model);
}
protected getStore(): Store<AppState, AnyAction> {
return this.notebookClient.getStore();
}
}

View File

@@ -0,0 +1,69 @@
import { ServerConfig, IContentProvider, FileType, IContent, IGetParams } from "@nteract/core";
import { Observable } from "rxjs";
import { AjaxResponse } from "rxjs/ajax";
import { GitHubContentProvider } from "../../../GitHub/GitHubContentProvider";
import { GitHubUtils } from "../../../Utils/GitHubUtils";
export class NotebookContentProvider implements IContentProvider {
constructor(private gitHubContentProvider: GitHubContentProvider, private jupyterContentProvider: IContentProvider) {}
public remove(serverConfig: ServerConfig, path: string): Observable<AjaxResponse> {
return this.getContentProvider(path).remove(serverConfig, path);
}
public get(serverConfig: ServerConfig, path: string, params: Partial<IGetParams>): Observable<AjaxResponse> {
return this.getContentProvider(path).get(serverConfig, path, params);
}
public update<FT extends FileType>(
serverConfig: ServerConfig,
path: string,
model: Partial<IContent<FT>>
): Observable<AjaxResponse> {
return this.getContentProvider(path).update(serverConfig, path, model);
}
public create<FT extends FileType>(
serverConfig: ServerConfig,
path: string,
model: Partial<IContent<FT>> & { type: FT }
): Observable<AjaxResponse> {
return this.getContentProvider(path).create(serverConfig, path, model);
}
public save<FT extends FileType>(
serverConfig: ServerConfig,
path: string,
model: Partial<IContent<FT>>
): Observable<AjaxResponse> {
return this.getContentProvider(path).save(serverConfig, path, model);
}
public listCheckpoints(serverConfig: ServerConfig, path: string): Observable<AjaxResponse> {
return this.getContentProvider(path).listCheckpoints(serverConfig, path);
}
public createCheckpoint(serverConfig: ServerConfig, path: string): Observable<AjaxResponse> {
return this.getContentProvider(path).createCheckpoint(serverConfig, path);
}
public deleteCheckpoint(serverConfig: ServerConfig, path: string, checkpointID: string): Observable<AjaxResponse> {
return this.getContentProvider(path).deleteCheckpoint(serverConfig, path, checkpointID);
}
public restoreFromCheckpoint(
serverConfig: ServerConfig,
path: string,
checkpointID: string
): Observable<AjaxResponse> {
return this.getContentProvider(path).restoreFromCheckpoint(serverConfig, path, checkpointID);
}
private getContentProvider(path: string): IContentProvider {
if (GitHubUtils.fromGitHubUri(path)) {
return this.gitHubContentProvider;
}
return this.jupyterContentProvider;
}
}

View File

@@ -0,0 +1,79 @@
import * as React from "react";
import { AppState, ContentRef, selectors } from "@nteract/core";
import { connect } from "react-redux";
import NteractUtil from "../NTeractUtil";
interface VirtualCommandBarComponentProps {
kernelSpecName: string;
kernelStatus: string;
currentCellType: string;
onRender: () => void;
}
class VirtualCommandBarComponent extends React.Component<VirtualCommandBarComponentProps> {
constructor(props: VirtualCommandBarComponentProps) {
super(props);
this.state = {};
}
shouldComponentUpdate(nextProps: VirtualCommandBarComponentProps): boolean {
return (
this.props.kernelStatus !== nextProps.kernelStatus ||
this.props.kernelSpecName !== nextProps.kernelSpecName ||
this.props.currentCellType !== nextProps.currentCellType
);
}
public render(): JSX.Element {
this.props.onRender && this.props.onRender();
return <></>;
}
}
interface InitialProps {
contentRef: ContentRef;
onRender: () => void;
}
// Redux
const makeMapStateToProps = (
initialState: AppState,
initialProps: InitialProps
): ((state: AppState) => VirtualCommandBarComponentProps) => {
const { contentRef } = initialProps;
const mapStateToProps = (state: AppState) => {
const content = selectors.content(state, { contentRef });
let kernelStatus, kernelSpecName, currentCellType;
if (!content || content.type !== "notebook") {
return {
kernelStatus,
kernelSpecName,
currentCellType
} as VirtualCommandBarComponentProps;
}
const kernelRef = content.model.kernelRef;
let kernel;
if (kernelRef) {
kernel = selectors.kernel(state, { kernelRef });
}
if (kernel) {
kernelStatus = kernel.status;
kernelSpecName = kernel.kernelSpecName;
}
currentCellType = NteractUtil.getCurrentCellType(content);
return {
kernelStatus,
kernelSpecName,
currentCellType,
onRender: initialProps.onRender
};
};
return mapStateToProps;
};
export default connect(makeMapStateToProps)(VirtualCommandBarComponent);

View File

@@ -0,0 +1,20 @@
import { Observable, of } from "rxjs";
import { AjaxResponse } from "rxjs/ajax";
import { ServerConfig } from "rx-jupyter";
let fakeAjaxResponse: AjaxResponse = {
originalEvent: undefined,
xhr: new XMLHttpRequest(),
request: null,
status: 200,
response: {},
responseText: null,
responseType: "json"
};
export const sessions = {
create: (serverConfig: ServerConfig, body: object): Observable<AjaxResponse> => of(fakeAjaxResponse),
__setResponse: (response: AjaxResponse) => {
fakeAjaxResponse = response;
},
createSpy: undefined as any
};

View File

@@ -0,0 +1,83 @@
import { ContentRef } from "@nteract/core";
import { CellId } from "@nteract/commutable";
export const CLOSE_NOTEBOOK = "CLOSE_NOTEBOOK";
export interface CloseNotebookAction {
type: "CLOSE_NOTEBOOK";
payload: {
contentRef: ContentRef;
};
}
export const closeNotebook = (payload: { contentRef: ContentRef }): CloseNotebookAction => {
return {
type: CLOSE_NOTEBOOK,
payload
};
};
export const UPDATE_LAST_MODIFIED = "UPDATE_LAST_MODIFIED";
export interface UpdateLastModifiedAction {
type: "UPDATE_LAST_MODIFIED";
payload: {
contentRef: ContentRef;
lastModified: string;
};
}
export const updateLastModified = (payload: {
contentRef: ContentRef;
lastModified: string;
}): UpdateLastModifiedAction => {
return {
type: UPDATE_LAST_MODIFIED,
payload
};
};
export const EXECUTE_FOCUSED_CELL_AND_FOCUS_NEXT = "EXECUTE_FOCUSED_CELL_AND_FOCUS_NEXT";
export interface ExecuteFocusedCellAndFocusNextAction {
type: "EXECUTE_FOCUSED_CELL_AND_FOCUS_NEXT";
payload: {
contentRef: ContentRef;
};
}
export const executeFocusedCellAndFocusNext = (payload: {
contentRef: ContentRef;
}): ExecuteFocusedCellAndFocusNextAction => {
return {
type: EXECUTE_FOCUSED_CELL_AND_FOCUS_NEXT,
payload
};
};
export const UPDATE_KERNEL_RESTART_DELAY = "UPDATE_KERNEL_RESTART_DELAY";
export interface UpdateKernelRestartDelayAction {
type: "UPDATE_KERNEL_RESTART_DELAY";
payload: {
delayMs: number;
};
}
export const UpdateKernelRestartDelay = (payload: { delayMs: number }): UpdateKernelRestartDelayAction => {
return {
type: UPDATE_KERNEL_RESTART_DELAY,
payload
};
};
export const SET_HOVERED_CELL = "SET_HOVERED_CELL";
export interface SetHoveredCellAction {
type: "SET_HOVERED_CELL";
payload: {
cellId: CellId;
};
}
export const setHoveredCell = (payload: { cellId: CellId }): SetHoveredCellAction => {
return {
type: SET_HOVERED_CELL,
payload
};
};

View File

@@ -0,0 +1,97 @@
import { AppState, ContentRef, selectors } from "@nteract/core";
import * as React from "react";
import { connect } from "react-redux";
import styled from "styled-components";
import NotebookRenderer from "../../../NotebookRenderer/NotebookRenderer";
import * as TextFile from "./text-file";
const PaddedContainer = styled.div`
padding-left: var(--nt-spacing-l, 10px);
padding-top: var(--nt-spacing-m, 10px);
padding-right: var(--nt-spacing-m, 10px);
`;
const JupyterExtensionContainer = styled.div`
display: flex;
flex-flow: column;
align-items: stretch;
height: 100%;
`;
const JupyterExtensionChoiceContainer = styled.div`
flex: 1 1 auto;
overflow: auto;
`;
interface FileProps {
type: "notebook" | "file" | "dummy";
contentRef: ContentRef;
mimetype?: string | null;
}
export class File extends React.PureComponent<FileProps> {
getChoice = () => {
let choice = null;
// notebooks don't report a mimetype so we'll use the content.type
if (this.props.type === "notebook") {
choice = <NotebookRenderer contentRef={this.props.contentRef} />;
} else if (this.props.type === "dummy") {
choice = null;
} else if (this.props.mimetype == null || !TextFile.handles(this.props.mimetype)) {
// This should not happen as we intercept mimetype upstream, but just in case
choice = (
<PaddedContainer>
<pre>
This file type cannot be rendered. Please download the file, in order to view it outside of Data Explorer.
</pre>
</PaddedContainer>
);
} else {
choice = <TextFile.default contentRef={this.props.contentRef} />;
}
return choice;
};
render(): JSX.Element {
const choice = this.getChoice();
// Right now we only handle one kind of editor
// If/when we support more modes, we would case them off here
return (
<React.Fragment>
<JupyterExtensionContainer>
<JupyterExtensionChoiceContainer>{choice}</JupyterExtensionChoiceContainer>
</JupyterExtensionContainer>
</React.Fragment>
);
}
}
interface InitialProps {
contentRef: ContentRef;
}
// Since the contentRef stays unique for the duration of this file,
// we use the makeMapStateToProps pattern to optimize re-render
const makeMapStateToProps = (initialState: AppState, initialProps: InitialProps) => {
const { contentRef } = initialProps;
const mapStateToProps = (state: AppState) => {
const content = selectors.content(state, initialProps);
return {
contentRef,
mimetype: content.mimetype,
type: content.type
};
};
return mapStateToProps;
};
export const ConnectedFile = connect(makeMapStateToProps)(File);
export default ConnectedFile;

View File

@@ -0,0 +1,143 @@
import { StringUtils } from "../../../../../Utils/StringUtils";
import { actions, AppState, ContentRef, selectors } from "@nteract/core";
import { MonacoEditorProps } from "@nteract/monaco-editor";
import * as React from "react";
import { connect } from "react-redux";
import { Dispatch } from "redux";
import styled from "styled-components";
const EditorContainer = styled.div`
position: absolute;
left: 0;
height: 100%;
width: 100%;
.monaco {
height: 100%;
}
`;
interface MappedStateProps {
mimetype: string;
text: string;
contentRef: ContentRef;
theme: string; // "light" | "dark";
}
interface MappedDispatchProps {
handleChange: (value: string) => void;
}
type TextFileProps = MappedStateProps & MappedDispatchProps;
interface TextFileState {
Editor: React.ComponentType<MonacoEditorProps>;
}
class EditorPlaceholder extends React.PureComponent<MonacoEditorProps> {
render(): JSX.Element {
// TODO: Show a little blocky placeholder
return null;
}
}
export class TextFile extends React.PureComponent<TextFileProps, TextFileState> {
constructor(props: TextFileProps) {
super(props);
this.state = {
Editor: EditorPlaceholder
};
}
handleChange = (source: string) => {
this.props.handleChange(source);
};
componentDidMount(): void {
import(/* webpackChunkName: "monaco-editor" */ "@nteract/monaco-editor").then(module => {
this.setState({ Editor: module.default });
});
}
render(): JSX.Element {
const Editor = this.state.Editor;
return (
<EditorContainer className="nteract-editor" style={{ position: "static" }}>
<Editor
theme={this.props.theme === "dark" ? "vs-dark" : "vs"}
mode={this.props.mimetype}
editorFocused
value={this.props.text}
onChange={this.handleChange.bind(this)}
/>
</EditorContainer>
);
}
}
interface InitialProps {
contentRef: ContentRef;
}
function makeMapStateToTextFileProps(
initialState: AppState,
initialProps: InitialProps
): (state: AppState) => MappedStateProps {
const { contentRef } = initialProps;
const mapStateToTextFileProps = (state: AppState) => {
const content = selectors.content(state, { contentRef });
if (!content || content.type !== "file") {
throw new Error("The text file component must have content");
}
const text = content.model ? content.model.text : "";
return {
contentRef,
mimetype: content.mimetype != null ? content.mimetype : "text/plain",
text,
theme: selectors.currentTheme(state)
};
};
return mapStateToTextFileProps;
}
const makeMapDispatchToTextFileProps = (
initialDispatch: Dispatch,
initialProps: InitialProps
): ((dispatch: Dispatch) => MappedDispatchProps) => {
const { contentRef } = initialProps;
const mapDispatchToTextFileProps = (dispatch: Dispatch) => {
return {
handleChange: (source: string) => {
dispatch(
actions.updateFileText({
contentRef,
text: source
})
);
}
};
};
return mapDispatchToTextFileProps;
};
const ConnectedTextFile = connect<MappedStateProps, MappedDispatchProps, InitialProps, AppState>(
makeMapStateToTextFileProps,
makeMapDispatchToTextFileProps
)(TextFile);
export function handles(mimetype: string) {
return (
!mimetype ||
StringUtils.startsWith(mimetype, "text/") ||
StringUtils.startsWith(mimetype, "application/javascript") ||
StringUtils.startsWith(mimetype, "application/json") ||
StringUtils.startsWith(mimetype, "application/x-ipynb+json")
);
}
export default ConnectedTextFile;

View File

@@ -0,0 +1,173 @@
// Vendor modules
import { CellType, ImmutableNotebook } from "@nteract/commutable";
import { HeaderDataProps } from "@nteract/connected-components/lib/header-editor";
import {
AppState,
ContentRef,
HostRecord,
selectors,
actions,
DirectoryContentRecordProps,
DummyContentRecordProps,
FileContentRecordProps,
NotebookContentRecordProps
} from "@nteract/core";
import { RecordOf } from "immutable";
import * as React from "react";
import { HotKeys, KeyMap } from "react-hotkeys";
import { connect } from "react-redux";
import { Dispatch } from "redux";
// Local modules
import { default as File } from "./file";
interface IContentsBaseProps {
contentRef: ContentRef;
error?: object | null;
}
interface IStateToProps {
headerData?: HeaderDataProps;
}
interface IDispatchFromProps {
handlers?: any;
onHeaderEditorChange?: (props: HeaderDataProps) => void;
}
type ContentsProps = IContentsBaseProps & IStateToProps & IDispatchFromProps;
class Contents extends React.PureComponent<ContentsProps> {
private keyMap: KeyMap = {
CHANGE_CELL_TYPE: ["ctrl+shift+y", "ctrl+shift+m", "meta+shift+y", "meta+shift+m"],
COPY_CELL: ["ctrl+shift+c", "meta+shift+c"],
CREATE_CELL_ABOVE: ["ctrl+shift+a", "meta+shift+a"],
CREATE_CELL_BELOW: ["ctrl+shift+b", "meta+shift+b"],
CUT_CELL: ["ctrl+shift+x", "meta+shift+x"],
DELETE_CELL: ["ctrl+shift+d", "meta+shift+d"],
EXECUTE_ALL_CELLS: ["alt+r a"],
INTERRUPT_KERNEL: ["alt+r i"],
KILL_KERNEL: ["alt+r k"],
OPEN: ["ctrl+o", "meta+o"],
PASTE_CELL: ["ctrl+shift+v"],
RESTART_KERNEL: ["alt+r r", "alt+r c", "alt+r a"],
SAVE: ["ctrl+s", "ctrl+shift+s", "meta+s", "meta+shift+s"]
};
render(): JSX.Element {
const { contentRef, handlers } = this.props;
if (!contentRef) {
return <></>;
}
return (
<React.Fragment>
<HotKeys keyMap={this.keyMap} handlers={handlers} className="hotKeys">
<File contentRef={contentRef} />
</HotKeys>
</React.Fragment>
);
}
}
const makeMapStateToProps: any = (initialState: AppState, initialProps: { contentRef: ContentRef }) => {
const host: HostRecord = initialState.app.host;
if (host.type !== "jupyter") {
throw new Error("this component only works with jupyter apps");
}
const mapStateToProps = (state: AppState): Partial<ContentsProps> => {
const contentRef: ContentRef = initialProps.contentRef;
if (!contentRef) {
throw new Error("cant display without a contentRef");
}
const content:
| RecordOf<NotebookContentRecordProps>
| RecordOf<DummyContentRecordProps>
| RecordOf<FileContentRecordProps>
| RecordOf<DirectoryContentRecordProps>
| undefined = selectors.content(state, { contentRef });
if (!content) {
return {
contentRef: undefined,
error: undefined,
headerData: undefined
};
}
let headerData: HeaderDataProps = {
authors: [],
description: "",
tags: [],
title: ""
};
// If a notebook, we need to read in the headerData if available
if (content.type === "notebook") {
const notebook: ImmutableNotebook = content.model.get("notebook");
const metadata: any = notebook.metadata.toJS();
const { authors = [], description = "", tags = [], title = "" } = metadata;
// Updates
headerData = Object.assign({}, headerData, {
authors,
description,
tags,
title
});
}
return {
contentRef,
error: content.error,
headerData
};
};
return mapStateToProps;
};
const mapDispatchToProps = (dispatch: Dispatch, ownProps: ContentsProps): object => {
const { contentRef } = ownProps;
return {
onHeaderEditorChange: (props: HeaderDataProps) => {
return dispatch(
actions.overwriteMetadataFields({
...props,
contentRef: ownProps.contentRef
})
);
},
// `HotKeys` handlers object
// see: https://github.com/greena13/react-hotkeys#defining-handlers
handlers: {
CHANGE_CELL_TYPE: (event: KeyboardEvent) => {
const type: CellType = event.key === "Y" ? "code" : "markdown";
return dispatch(actions.changeCellType({ to: type, contentRef }));
},
COPY_CELL: () => dispatch(actions.copyCell({ contentRef })),
CREATE_CELL_ABOVE: () => dispatch(actions.createCellAbove({ cellType: "code", contentRef })),
CREATE_CELL_BELOW: () => dispatch(actions.createCellBelow({ cellType: "code", source: "", contentRef })),
CUT_CELL: () => dispatch(actions.cutCell({ contentRef })),
DELETE_CELL: () => dispatch(actions.deleteCell({ contentRef })),
EXECUTE_ALL_CELLS: () => dispatch(actions.executeAllCells({ contentRef })),
INTERRUPT_KERNEL: () => dispatch(actions.interruptKernel({})),
KILL_KERNEL: () => dispatch(actions.killKernel({ restarting: false })),
PASTE_CELL: () => dispatch(actions.pasteCell({ contentRef })),
RESTART_KERNEL: (event: KeyboardEvent) => {
const outputHandling: "None" | "Clear All" | "Run All" =
event.key === "r" ? "None" : event.key === "a" ? "Run All" : "Clear All";
return dispatch(actions.restartKernel({ outputHandling, contentRef }));
},
SAVE: () => dispatch(actions.save({ contentRef }))
}
};
};
export default connect(makeMapStateToProps, mapDispatchToProps)(Contents);

View File

@@ -0,0 +1,492 @@
import * as Immutable from "immutable";
import { ActionsObservable, StateObservable } from "redux-observable";
import { Subject } from "rxjs";
import { toArray } from "rxjs/operators";
import { makeNotebookRecord } from "@nteract/commutable";
import { actions, state } from "@nteract/core";
import * as sinon from "sinon";
import { CdbAppState, makeCdbRecord } from "./types";
import { launchWebSocketKernelEpic } from "./epics";
import { NotebookUtil } from "../NotebookUtil";
import { sessions } from "rx-jupyter";
describe("Extract kernel from notebook", () => {
it("Reads metadata kernelspec first", () => {
const fakeNotebook = makeNotebookRecord({
metadata: Immutable.Map({
kernelspec: {
display_name: "Python 3",
language: "python",
name: "python3"
},
language_info: {
name: "python",
version: "3.7.3",
mimetype: "text/x-python",
codemirror_mode: {
name: "ipython",
version: 3
},
pygments_lexer: "ipython3",
nbconvert_exporter: "python",
file_extension: ".py"
}
})
});
const result = NotebookUtil.extractNewKernel("blah", fakeNotebook);
expect(result.kernelSpecName).toEqual("python3");
});
it("Reads language info in metadata if kernelspec not present", () => {
const fakeNotebook = makeNotebookRecord({
metadata: Immutable.Map({
language_info: {
name: "python",
version: "3.7.3",
mimetype: "text/x-python",
codemirror_mode: {
name: "ipython",
version: 3
},
pygments_lexer: "ipython3",
nbconvert_exporter: "python",
file_extension: ".py"
}
})
});
const result = NotebookUtil.extractNewKernel("blah", fakeNotebook);
expect(result.kernelSpecName).toEqual("python");
});
it("Returns nothing if no kernelspec nor language info is found in metadata", () => {
const fakeNotebook = makeNotebookRecord({
metadata: Immutable.Map({
blah: "this should be ignored"
})
});
const result = NotebookUtil.extractNewKernel("blah", fakeNotebook);
expect(result.kernelSpecName).toEqual(undefined);
});
});
describe("launchWebSocketKernelEpic", () => {
const createSpy = sinon.spy(sessions, "create");
const contentRef = "fakeContentRef";
const kernelRef = "fake";
const initialState = {
app: state.makeAppRecord({
host: state.makeJupyterHostRecord({
type: "jupyter",
token: "eh",
basePath: "/"
})
}),
comms: state.makeCommsRecord(),
config: Immutable.Map({}),
core: state.makeStateRecord({
kernelRef: "fake",
entities: state.makeEntitiesRecord({
contents: state.makeContentsRecord({
byRef: Immutable.Map({
fakeContentRef: state.makeNotebookContentRecord()
})
}),
kernels: state.makeKernelsRecord({
byRef: Immutable.Map({
fake: state.makeRemoteKernelRecord({
type: "websocket",
channels: new Subject<any>(),
kernelSpecName: "fancy",
id: "0"
})
})
})
})
}),
cdb: makeCdbRecord({
databaseAccountName: "dbAccountName",
defaultExperience: "defaultExperience"
})
};
it("launches remote kernels", async () => {
const state$ = new StateObservable(new Subject<CdbAppState>(), initialState);
const cwd = "/";
const kernelId = "123";
const kernelSpecName = "kernelspecname";
const sessionId = "sessionId";
const action$ = ActionsObservable.of(
actions.launchKernelByName({
contentRef,
kernelRef,
kernelSpecName,
cwd,
selectNextKernel: true
})
);
(sessions as any).__setResponse({
originalEvent: undefined,
xhr: new XMLHttpRequest(),
request: null,
status: 200,
response: {
id: sessionId,
path: "notebooks/Untitled7.ipynb",
name: "",
type: "notebook",
kernel: {
id: kernelId,
name: "kernel_launched",
last_activity: "2019-11-07T14:29:54.432454Z",
execution_state: "starting",
connections: 0
},
notebook: {
path: "notebooks/Untitled7.ipynb",
name: ""
}
},
responseText: null,
responseType: "json"
});
const responseActions = await launchWebSocketKernelEpic(action$, state$)
.pipe(toArray())
.toPromise();
expect(responseActions).toMatchObject([
{
type: actions.LAUNCH_KERNEL_SUCCESSFUL,
payload: {
contentRef,
kernelRef,
selectNextKernel: true,
kernel: {
info: null,
sessionId: sessionId,
type: "websocket",
kernelSpecName,
cwd,
id: kernelId
}
}
}
]);
});
it("launches any kernel with no kernelspecs in the state", async () => {
const state$ = new StateObservable(new Subject<CdbAppState>(), initialState);
const cwd = "/";
const kernelId = "123";
const kernelSpecName = "kernelspecname";
const sessionId = "sessionId";
const action$ = ActionsObservable.of(
actions.launchKernelByName({
contentRef,
kernelRef,
kernelSpecName,
cwd,
selectNextKernel: true
})
);
(sessions as any).__setResponse({
originalEvent: undefined,
xhr: new XMLHttpRequest(),
request: null,
status: 200,
response: {
id: sessionId,
path: "notebooks/Untitled7.ipynb",
name: "",
type: "notebook",
kernel: {
id: kernelId,
name: "kernel_launched",
last_activity: "2019-11-07T14:29:54.432454Z",
execution_state: "starting",
connections: 0
},
notebook: {
path: "notebooks/Untitled7.ipynb",
name: ""
}
},
responseText: null,
responseType: "json"
});
await launchWebSocketKernelEpic(action$, state$)
.pipe(toArray())
.toPromise();
expect(createSpy.lastCall.args[1]).toMatchObject({
kernel: {
name: kernelSpecName
}
});
});
it("launches no kernel if no kernel is specified and state has no kernelspecs", async () => {
const state$ = new StateObservable(new Subject<CdbAppState>(), initialState);
const cwd = "/";
const kernelId = "123";
const kernelSpecName = "kernelspecname";
const sessionId = "sessionId";
const action$ = ActionsObservable.of(
actions.launchKernelByName({
contentRef,
kernelRef,
kernelSpecName: undefined,
cwd,
selectNextKernel: true
})
);
(sessions as any).__setResponse({
originalEvent: undefined,
xhr: new XMLHttpRequest(),
request: null,
status: 200,
response: {
id: sessionId,
path: "notebooks/Untitled7.ipynb",
name: "",
type: "notebook",
kernel: {
id: kernelId,
name: "kernel_launched",
last_activity: "2019-11-07T14:29:54.432454Z",
execution_state: "starting",
connections: 0
},
notebook: {
path: "notebooks/Untitled7.ipynb",
name: ""
}
},
responseText: null,
responseType: "json"
});
const responseActions = await launchWebSocketKernelEpic(action$, state$)
.pipe(toArray())
.toPromise();
expect(responseActions).toMatchObject([
{
type: actions.LAUNCH_KERNEL_FAILED
}
]);
});
it("emits an error if backend returns an error", async () => {
const state$ = new StateObservable(new Subject<CdbAppState>(), initialState);
const cwd = "/";
const action$ = ActionsObservable.of(
actions.launchKernelByName({
contentRef,
kernelRef,
kernelSpecName: undefined,
cwd,
selectNextKernel: true
})
);
(sessions as any).__setResponse({
originalEvent: undefined,
xhr: new XMLHttpRequest(),
request: null,
status: 500,
response: null,
responseText: null,
responseType: "json"
});
const responseActions = await launchWebSocketKernelEpic(action$, state$)
.pipe(toArray())
.toPromise();
expect(responseActions).toMatchObject([
{
type: actions.LAUNCH_KERNEL_FAILED
}
]);
});
describe("Choose correct kernelspecs to launch", () => {
beforeAll(() => {
// Initialize kernelspecs with 2 supported kernels
const createKernelSpecsRecord = (): Immutable.RecordOf<state.KernelspecsRecordProps> =>
state.makeKernelspecsRecord({
byRef: Immutable.Map({
kernelspecsref: state.makeKernelspecsByRefRecord({
defaultKernelName: "kernel2",
byName: Immutable.Map({
kernel1: state.makeKernelspec({
name: "kernel1",
argv: Immutable.List([]),
env: Immutable.Map(),
interruptMode: "interruptMode1",
language: "language1",
displayName: "Kernel One",
metadata: Immutable.Map(),
resources: Immutable.Map()
}),
kernel2: state.makeKernelspec({
name: "kernel2",
argv: Immutable.List([]),
env: Immutable.Map(),
interruptMode: "interruptMode2",
language: "language2",
displayName: "Kernel Two",
metadata: Immutable.Map(),
resources: Immutable.Map()
})
})
})
}),
refs: Immutable.List(["kernelspecsref"])
});
initialState.core = initialState.core
.setIn(["entities", "kernelspecs"], createKernelSpecsRecord())
.set("currentKernelspecsRef", "kernelspecsref");
// some fake response we don't care about
(sessions as any).__setResponse({
originalEvent: undefined,
xhr: new XMLHttpRequest(),
request: null,
status: 200,
response: {
id: "sessionId",
path: "notebooks/Untitled7.ipynb",
name: "",
type: "notebook",
kernel: {
id: "kernelId",
name: "kernel_launched",
last_activity: "2019-11-07T14:29:54.432454Z",
execution_state: "starting",
connections: 0
},
notebook: {
path: "notebooks/Untitled7.ipynb",
name: ""
}
},
responseText: null,
responseType: "json"
});
});
it("launches supported kernel in kernelspecs", async () => {
const state$ = new StateObservable(new Subject<CdbAppState>(), initialState);
const action$ = ActionsObservable.of(
actions.launchKernelByName({
contentRef,
kernelRef,
kernelSpecName: "kernel2",
cwd: "cwd",
selectNextKernel: true
})
);
await launchWebSocketKernelEpic(action$, state$)
.pipe(toArray())
.toPromise();
expect(createSpy.lastCall.args[1]).toMatchObject({
kernel: {
name: "kernel2"
}
});
});
it("launches undefined kernel uses default kernel from kernelspecs", async () => {
const state$ = new StateObservable(new Subject<CdbAppState>(), initialState);
const action$ = ActionsObservable.of(
actions.launchKernelByName({
contentRef,
kernelRef,
kernelSpecName: undefined,
cwd: "cwd",
selectNextKernel: true
})
);
await launchWebSocketKernelEpic(action$, state$)
.pipe(toArray())
.toPromise();
expect(createSpy.lastCall.args[1]).toMatchObject({
kernel: {
name: "kernel2"
}
});
});
it("launches unsupported kernel uses default kernel from kernelspecs", async () => {
const state$ = new StateObservable(new Subject<CdbAppState>(), initialState);
const action$ = ActionsObservable.of(
actions.launchKernelByName({
contentRef,
kernelRef,
kernelSpecName: "This is an unknown kernelspec",
cwd: "cwd",
selectNextKernel: true
})
);
await launchWebSocketKernelEpic(action$, state$)
.pipe(toArray())
.toPromise();
expect(createSpy.lastCall.args[1]).toMatchObject({
kernel: {
name: "kernel2"
}
});
});
it("launches unsupported kernel uses kernelspecs with similar name", async () => {
const state$ = new StateObservable(new Subject<CdbAppState>(), initialState);
const action$ = ActionsObservable.of(
actions.launchKernelByName({
contentRef,
kernelRef,
kernelSpecName: "ernel1",
cwd: "cwd",
selectNextKernel: true
})
);
await launchWebSocketKernelEpic(action$, state$)
.pipe(toArray())
.toPromise();
expect(createSpy.lastCall.args[1]).toMatchObject({
kernel: {
name: "kernel1"
}
});
});
});
});

View File

@@ -0,0 +1,925 @@
import { empty, merge, of, timer, interval, concat, Subject, Subscriber, Observable, Observer } from "rxjs";
import { webSocket } from "rxjs/webSocket";
import { ActionsObservable, StateObservable } from "redux-observable";
import { ofType } from "redux-observable";
import {
mergeMap,
tap,
retryWhen,
delayWhen,
map,
switchMap,
take,
distinctUntilChanged,
filter,
catchError,
first,
concatMap,
timeout
} from "rxjs/operators";
import {
AppState,
ServerConfig as JupyterServerConfig,
JupyterHostRecordProps,
JupyterHostRecord,
RemoteKernelProps,
castToSessionId,
createKernelRef,
KernelRef,
ContentRef,
KernelInfo,
actions,
selectors,
IContentProvider
} from "@nteract/core";
import { message, JupyterMessage, Channels, createMessage, childOf, ofMessageType } from "@nteract/messaging";
import { sessions, kernels } from "rx-jupyter";
import { RecordOf } from "immutable";
import * as Constants from "../../../Common/Constants";
import { NotificationConsoleUtils } from "../../../Utils/NotificationConsoleUtils";
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
import * as CdbActions from "./actions";
import TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import { Action as TelemetryAction } from "../../../Shared/Telemetry/TelemetryConstants";
import { CdbAppState } from "./types";
import { decryptJWTToken } from "../../../Utils/AuthorizationUtils";
import * as TextFile from "./contents/file/text-file";
import { NotebookUtil } from "../NotebookUtil";
interface NotebookServiceConfig extends JupyterServerConfig {
userPuid?: string;
}
const logToTelemetry = (state: CdbAppState, title: string, error?: string) => {
TelemetryProcessor.traceFailure(TelemetryAction.NotebookErrorNotification, {
databaseAccountName: state.cdb.databaseAccountName,
defaultExperience: state.cdb.defaultExperience,
dataExplorerArea: Constants.Areas.Notebook,
title,
error
});
};
/**
* Automatically add a new cell if notebook is empty
* @param action$
* @param state$
*/
const addInitialCodeCellEpic = (
action$: ActionsObservable<actions.FetchContentFulfilled>,
state$: StateObservable<AppState>
): Observable<{} | actions.CreateCellBelow> => {
return action$.pipe(
ofType(actions.FETCH_CONTENT_FULFILLED),
mergeMap(action => {
const state = state$.value;
const contentRef = action.payload.contentRef;
const model = selectors.model(state, { contentRef });
// If it's not a notebook, we shouldn't be here
if (!model || model.type !== "notebook") {
return empty();
}
const cellOrder = selectors.notebook.cellOrder(model);
if (cellOrder.size === 0) {
return of(
actions.createCellAppend({
cellType: "code",
contentRef
})
);
}
return empty();
})
);
};
/**
* Updated kernels.formWebSocketURL so we pass the userId as a query param
*/
const formWebSocketURL = (serverConfig: NotebookServiceConfig, kernelId: string, sessionId?: string): string => {
const params = new URLSearchParams();
if (serverConfig.token) {
params.append("token", serverConfig.token);
}
if (sessionId) {
params.append("session_id", sessionId);
}
const userId = getUserPuid();
if (userId) {
params.append("user_id", userId);
}
const q = params.toString();
const suffix = q !== "" ? `?${q}` : "";
const url = (serverConfig.endpoint || "") + `api/kernels/${kernelId}/channels${suffix}`;
return url.replace(/^http(s)?/, "ws$1");
};
/**
* Override from kernel-lifecycle to improve code mirror language intellisense
* @param action$
*/
export const acquireKernelInfoEpic = (action$: ActionsObservable<actions.NewKernelAction>) => {
return action$.pipe(
ofType(actions.LAUNCH_KERNEL_SUCCESSFUL),
switchMap((action: actions.NewKernelAction) => {
const {
payload: {
kernel: { channels },
kernelRef,
contentRef
}
} = action;
return acquireKernelInfo(channels, kernelRef, contentRef);
})
);
};
/**
* Send a kernel_info_request to the kernel and derive code mirror mode based on the language name.
*/
function acquireKernelInfo(channels: Channels, kernelRef: KernelRef, contentRef: ContentRef) {
const message = createMessage("kernel_info_request");
const obs = channels.pipe(
childOf(message),
ofMessageType("kernel_info_reply"),
first(),
mergeMap(msg => {
const content = msg.content;
const languageInfo = (content && content.language_info) || {
name: "",
version: "",
mimetype: "",
file_extension: "",
pygments_lexer: "",
codemirror_mode: "",
nbconvert_exporter: ""
};
switch (languageInfo.name) {
case "csharp":
languageInfo.codemirror_mode = "text/x-csharp";
break;
case "scala":
languageInfo.codemirror_mode = "text/x-scala";
break;
}
const info: KernelInfo = {
protocolVersion: content.protocol_version,
implementation: content.implementation,
implementationVersion: content.implementation_version,
banner: content.banner,
helpLinks: content.help_links,
languageName: languageInfo.name,
languageVersion: languageInfo.version,
mimetype: languageInfo.mimetype,
fileExtension: languageInfo.file_extension,
pygmentsLexer: languageInfo.pygments_lexer,
codemirrorMode: languageInfo.codemirror_mode,
nbconvertExporter: languageInfo.nbconvert_exporter
};
let result;
if (!content.protocol_version.startsWith("5")) {
result = [
actions.launchKernelFailed({
kernelRef,
contentRef,
error: new Error(
"The kernel that you are attempting to launch does not support the latest version (v5) of the messaging protocol."
)
})
];
} else {
result = [
// The original action we were using
actions.setLanguageInfo({
langInfo: msg.content.language_info,
kernelRef,
contentRef
}),
actions.setKernelInfo({
kernelRef,
info
})
];
}
return of(...result);
})
);
return Observable.create((observer: Observer<any>) => {
const subscription = obs.subscribe(observer);
channels.next(message);
return subscription;
});
}
/**
* Updated kernels.connect so we use the updated formWebSocketURL to pass
* the userId as a query param
* @param serverConfig
* @param kernelID
* @param sessionID
*/
const connect = (serverConfig: NotebookServiceConfig, kernelID: string, sessionID?: string): Subject<any> => {
const wsSubject = webSocket<JupyterMessage>({
url: formWebSocketURL(serverConfig, kernelID, sessionID),
protocol: serverConfig.wsProtocol
});
// Create a subject that does some of the handling inline for the session
// and ensuring it's serialized
return Subject.create(
Subscriber.create(
(message?: JupyterMessage) => {
if (typeof message === "object") {
const sessionizedMessage = {
...message,
header: {
session: sessionID,
...message.header
}
};
wsSubject.next(sessionizedMessage);
} else {
console.error("Message must be an object, the app sent", message);
}
},
(e: Error) => wsSubject.error(e),
() => wsSubject.complete()
), // Subscriber
// Subject.create takes a subscriber and an observable. We're only
// overriding the subscriber here so we pass the subject on as an
// observable as the second argument to Subject.create (since it's
// _also_ an observable)
wsSubject
);
};
/**
* Override launch websocket kernel epic:
* - pass the userId
* - if kernelspecs are present in the state:
* * verify that the kernel name matches one of the kernelspecs
* * else attempt to pick a kernel that matches the name from the kernelspecs list
* * else pick the default kernel specs
* @param action$
* @param state$
*/
export const launchWebSocketKernelEpic = (
action$: ActionsObservable<actions.LaunchKernelByNameAction>,
state$: StateObservable<CdbAppState>
) => {
return action$.pipe(
ofType(actions.LAUNCH_KERNEL_BY_NAME),
// Only accept jupyter servers for the host with this epic
filter(() => selectors.isCurrentHostJupyter(state$.value)),
switchMap((action: actions.LaunchKernelByNameAction) => {
const state = state$.value;
const host = selectors.currentHost(state);
if (host.type !== "jupyter") {
return empty();
}
const serverConfig: NotebookServiceConfig = selectors.serverConfig(host);
serverConfig.userPuid = getUserPuid();
const {
payload: { kernelSpecName, cwd, kernelRef, contentRef }
} = action;
const content = selectors.content(state, { contentRef });
if (!content || content.type !== "notebook") {
return empty();
}
let kernelSpecToLaunch = kernelSpecName;
const currentKernelspecs = selectors.currentKernelspecs(state$.value);
if (!kernelSpecToLaunch) {
if (currentKernelspecs) {
kernelSpecToLaunch = currentKernelspecs.defaultKernelName;
const msg = `No kernelspec name specified to launch, using default kernel: ${kernelSpecToLaunch}`;
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, msg);
logToTelemetry(state$.value, "Launching alternate kernel", msg);
} else {
return of(
actions.launchKernelFailed({
error: new Error(
"Unable to launch kernel: no kernelspec name specified to launch and no default kernelspecs"
),
contentRef
})
);
}
} else if (currentKernelspecs && !currentKernelspecs.byName.get(kernelSpecToLaunch)) {
let msg = `Cannot launch kernelspec: "${kernelSpecToLaunch}" is not supported by the notebook server.`;
// Find a kernel that best matches the kernel name
const match = currentKernelspecs.byName.find(
value => value.name.toLowerCase().indexOf(kernelSpecName.toLowerCase()) !== -1
);
if (match) {
kernelSpecToLaunch = match.name;
msg += ` Found kernel with similar name: ${kernelSpecToLaunch}`;
} else {
kernelSpecToLaunch = currentKernelspecs.defaultKernelName;
msg += ` Using default kernel: ${kernelSpecToLaunch}`;
}
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, msg);
logToTelemetry(state$.value, "Launching alternate kernel", msg);
}
const sessionPayload = {
kernel: {
id: null,
name: kernelSpecToLaunch
} as any,
name: "",
path: content.filepath.replace(/^\/+/g, ""),
type: "notebook"
};
return sessions.create(serverConfig, sessionPayload).pipe(
mergeMap(data => {
const session = data.response;
const sessionId = castToSessionId(session.id);
const kernel: RemoteKernelProps = Object.assign({}, session.kernel, {
type: "websocket",
info: null,
sessionId,
cwd,
channels: connect(serverConfig, session.kernel.id, sessionId),
kernelSpecName: kernelSpecToLaunch
});
kernel.channels.next(message({ msg_type: "kernel_info_request" }));
return of(
actions.launchKernelSuccessful({
kernel,
kernelRef,
contentRef: action.payload.contentRef,
selectNextKernel: true
})
);
}),
catchError(error => {
return of(actions.launchKernelFailed({ error }));
})
);
})
);
};
/**
* Override the restartWebSocketKernelEpic from nteract since the /restart endpoint of our kernels has not
* been implmemented;
* TODO: Remove this epic once the /restart endpoint is implemented.
*/
export const restartWebSocketKernelEpic = (
action$: ActionsObservable<actions.RestartKernel | actions.NewKernelAction>,
state$: StateObservable<AppState>
) =>
action$.pipe(
ofType(actions.RESTART_KERNEL),
concatMap((action: actions.RestartKernel) => {
const state = state$.value;
const contentRef = action.payload.contentRef;
const kernelRef = selectors.kernelRefByContentRef(state, { contentRef }) || action.payload.kernelRef;
/**
* If there is still no KernelRef, then throw an error.
*/
if (!kernelRef) {
return of(
actions.restartKernelFailed({
error: new Error("Can't execute restart without kernel ref."),
kernelRef: "none provided",
contentRef
})
);
}
const host = selectors.currentHost(state);
if (host.type !== "jupyter") {
return of(
actions.restartKernelFailed({
error: new Error("Can't restart a kernel with no Jupyter host."),
kernelRef,
contentRef
})
);
}
const kernel = selectors.kernel(state, { kernelRef });
if (!kernel) {
return of(
actions.restartKernelFailed({
error: new Error("Can't restart a kernel that does not exist."),
kernelRef,
contentRef
})
);
}
if (kernel.type !== "websocket" || !kernel.id) {
return of(
actions.restartKernelFailed({
error: new Error("Can only restart Websocket kernels via API."),
kernelRef,
contentRef
})
);
}
const newKernelRef = createKernelRef();
const kill = actions.killKernel({
restarting: true,
kernelRef
});
const relaunch = actions.launchKernelByName({
kernelSpecName: kernel.kernelSpecName ?? undefined,
cwd: kernel.cwd,
kernelRef: newKernelRef,
selectNextKernel: true,
contentRef: contentRef
});
const awaitKernelReady = action$.pipe(
ofType(actions.LAUNCH_KERNEL_SUCCESSFUL),
filter((action: actions.NewKernelAction | actions.RestartKernel) => action.payload.kernelRef === newKernelRef),
take(1),
timeout(60000), // If kernel doesn't come up within this interval we will abort follow-on actions.
concatMap(() => {
const restartSuccess = actions.restartKernelSuccessful({
kernelRef: newKernelRef,
contentRef
});
if ((action as actions.RestartKernel).payload.outputHandling === "Run All") {
return of(restartSuccess, actions.executeAllCells({ contentRef }));
} else {
return of(restartSuccess);
}
}),
catchError(error => {
return of(
actions.restartKernelFailed({
error,
kernelRef: newKernelRef,
contentRef
})
);
})
);
return merge(of(kill, relaunch), awaitKernelReady);
})
);
/**
* Override changeWebSocketKernelEpic:
* - to pass the userId when connecting to the kernel.
* - to override extractNewKernel()
* @param action$
* @param state$
*/
const changeWebSocketKernelEpic = (
action$: ActionsObservable<actions.ChangeKernelByName>,
state$: StateObservable<AppState>
) => {
return action$.pipe(
ofType(actions.CHANGE_KERNEL_BY_NAME),
// Only accept jupyter servers for the host with this epic
filter(() => selectors.isCurrentHostJupyter(state$.value)),
switchMap((action: actions.ChangeKernelByName) => {
const {
payload: { contentRef, oldKernelRef, kernelSpecName }
} = action;
const state = state$.value;
const host = selectors.currentHost(state);
if (host.type !== "jupyter") {
return empty();
}
const serverConfig: NotebookServiceConfig = selectors.serverConfig(host);
if (!oldKernelRef) {
return empty();
}
const oldKernel = selectors.kernel(state, { kernelRef: oldKernelRef });
if (!oldKernel || oldKernel.type !== "websocket") {
return empty();
}
const { sessionId } = oldKernel;
if (!sessionId) {
return empty();
}
const content = selectors.content(state, { contentRef });
if (!content || content.type !== "notebook") {
return empty();
}
const {
filepath,
model: { notebook }
} = content;
const { cwd } = NotebookUtil.extractNewKernel(filepath, notebook);
const kernelRef = createKernelRef();
return kernels.start(serverConfig, kernelSpecName, cwd).pipe(
mergeMap(({ response }) => {
const { id: kernelId } = response;
const sessionPayload = {
kernel: { id: kernelId, name: kernelSpecName }
};
// The sessions API will close down the old kernel for us if it is on this session
return sessions.update(serverConfig, sessionId, sessionPayload).pipe(
mergeMap(({ response: session }) => {
const kernel: RemoteKernelProps = Object.assign({}, session.kernel, {
type: "websocket",
sessionId,
cwd,
channels: connect(serverConfig, session.kernel.id, sessionId),
kernelSpecName
});
return of(
actions.launchKernelSuccessful({
kernel,
kernelRef,
contentRef: action.payload.contentRef,
selectNextKernel: true
})
);
}),
catchError(error => of(actions.launchKernelFailed({ error, kernelRef, contentRef })))
);
}),
catchError(error => of(actions.launchKernelFailed({ error, kernelRef, contentRef })))
);
})
);
};
/**
* Automatically focus on cell if only one cell
* @param action$
* @param state$
*/
const focusInitialCodeCellEpic = (
action$: ActionsObservable<actions.CreateCellAppend>,
state$: StateObservable<AppState>
): Observable<{} | actions.FocusCell> => {
return action$.pipe(
ofType(actions.CREATE_CELL_APPEND),
mergeMap(action => {
const state = state$.value;
const contentRef = action.payload.contentRef;
const model = selectors.model(state, { contentRef });
// If it's not a notebook, we shouldn't be here
if (!model || model.type !== "notebook") {
return empty();
}
const cellOrder = selectors.notebook.cellOrder(model);
if (cellOrder.size === 1) {
const id = cellOrder.get(0);
// Focus on the cell
return of(
actions.focusCell({
id,
contentRef
})
);
}
return empty();
})
);
};
/**
* Capture some actions to display to notification console
* TODO: Log these (or everything) in telemetry?
* @param action$
* @param state$
*/
const notificationsToUserEpic = (
action$: ActionsObservable<any>,
state$: StateObservable<CdbAppState>
): Observable<{}> => {
return action$.pipe(
ofType(
actions.RESTART_KERNEL_SUCCESSFUL,
actions.RESTART_KERNEL_FAILED,
actions.SAVE_FULFILLED,
actions.SAVE_FAILED,
actions.FETCH_CONTENT_FAILED
),
mergeMap(action => {
switch (action.type) {
case actions.RESTART_KERNEL_SUCCESSFUL: {
const title = "Kernel restart";
const msg = "Kernel successfully restarted";
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, msg);
logToTelemetry(state$.value, title, msg);
break;
}
case actions.RESTART_KERNEL_FAILED:
// TODO: enable once incorrect kernel restart failure signals are fixed
// NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, "Failed to restart kernel");
break;
case actions.SAVE_FAILED: {
const title = "Save failure";
const msg = `Failed to save notebook: ${(action as actions.SaveFailed).payload.error}`;
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, msg);
logToTelemetry(state$.value, title, msg);
break;
}
case actions.FETCH_CONTENT_FAILED: {
const typedAction: actions.FetchContentFailed = action;
const filepath = selectors.filepath(state$.value, { contentRef: typedAction.payload.contentRef });
const title = "Fetching content failure";
const msg = `Failed to fetch notebook content: ${filepath}, error: ${typedAction.payload.error}`;
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, msg);
logToTelemetry(state$.value, title, msg);
break;
}
}
return empty();
})
);
};
/**
* Connection lost: ping server until back up and restart kernel
* @param action$
* @param state$
*/
const handleKernelConnectionLostEpic = (
action$: ActionsObservable<actions.UpdateDisplayFailed>,
state$: StateObservable<CdbAppState>
): Observable<CdbActions.UpdateKernelRestartDelayAction | actions.RestartKernel | {}> => {
return action$.pipe(
ofType(actions.UPDATE_DISPLAY_FAILED),
mergeMap(action => {
const state = state$.value;
const msg = "Notebook was disconnected from kernel";
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, msg);
logToTelemetry(state, "Error", "Kernel connection error");
const host = selectors.currentHost(state);
const serverConfig: NotebookServiceConfig = selectors.serverConfig(host as RecordOf<JupyterHostRecordProps>);
const contentRef = action.payload.contentRef;
const kernelRef = selectors.kernelRefByContentRef(state$.value, { contentRef });
const delayMs = state.cdb.kernelRestartDelayMs;
if (delayMs > Constants.Notebook.kernelRestartMaxDelayMs) {
const msg =
"Restarted kernel too many times. Please reload the page to enable Data Explorer to restart the kernel automatically.";
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, msg);
logToTelemetry(state, "Kernel restart error", msg);
const explorer = window.dataExplorer;
if (explorer) {
explorer.showOkModalDialog("kernel restarts", msg);
}
return of(empty());
}
return concat(
of(CdbActions.UpdateKernelRestartDelay({ delayMs: delayMs * 1.5 })),
sessions.list(serverConfig).pipe(
delayWhen(() => timer(delayMs)),
map(xhr => {
return actions.restartKernel({
outputHandling: "None",
kernelRef,
contentRef
});
}),
retryWhen(errors => {
return errors.pipe(
delayWhen(() => timer(Constants.Notebook.heartbeatDelayMs)),
tap(() => console.log("retrying...")) // TODO: Send new action?
);
})
)
);
})
);
};
/**
* Connection lost: clean up kernel ref
* @param action$
* @param state$
*/
export const cleanKernelOnConnectionLostEpic = (
action$: ActionsObservable<actions.UpdateDisplayFailed>,
state$: StateObservable<AppState>
): Observable<actions.KillKernelSuccessful> => {
return action$.pipe(
ofType(actions.UPDATE_DISPLAY_FAILED),
switchMap(action => {
const contentRef = action.payload.contentRef;
const kernelRef = selectors.kernelRefByContentRef(state$.value, { contentRef });
return of(
actions.killKernelSuccessful({
kernelRef
})
);
})
);
};
/**
* Workaround for issue: https://github.com/nteract/nteract/issues/4583
* We reajust the property
* @param action$
* @param state$
*/
const adjustLastModifiedOnSaveEpic = (
action$: ActionsObservable<actions.SaveFulfilled>,
state$: StateObservable<AppState>,
dependencies: { contentProvider: IContentProvider }
): Observable<{} | CdbActions.UpdateLastModifiedAction> => {
return action$.pipe(
ofType(actions.SAVE_FULFILLED),
mergeMap(action => {
const pollDelayMs = 500;
const nbAttempts = 4;
// Retry updating last modified
const currentHost = selectors.currentHost(state$.value);
const serverConfig = selectors.serverConfig(currentHost as JupyterHostRecord);
const filepath = selectors.filepath(state$.value, { contentRef: action.payload.contentRef });
const content = selectors.content(state$.value, { contentRef: action.payload.contentRef });
const lastSaved = (content.lastSaved as any) as string;
const contentProvider = dependencies.contentProvider;
// Query until value is stable
return interval(pollDelayMs)
.pipe(take(nbAttempts))
.pipe(
mergeMap(x =>
contentProvider.get(serverConfig, filepath, { content: 0 }).pipe(
map(xhr => {
if (xhr.status !== 200 || typeof xhr.response === "string") {
return undefined;
}
const model = xhr.response;
const lastModified = model.last_modified;
if (lastModified === lastSaved) {
return undefined;
}
// Return last modified
return lastModified;
})
)
),
distinctUntilChanged(),
mergeMap(lastModified => {
if (!lastModified) {
return empty();
}
return of(
CdbActions.updateLastModified({
contentRef: action.payload.contentRef,
lastModified
})
);
})
);
})
);
};
/**
* Execute focused cell and focus next cell
* @param action$
* @param state$
*/
const executeFocusedCellAndFocusNextEpic = (
action$: ActionsObservable<CdbActions.ExecuteFocusedCellAndFocusNextAction>,
state$: StateObservable<AppState>
): Observable<{} | actions.FocusNextCellEditor> => {
return action$.pipe(
ofType(CdbActions.EXECUTE_FOCUSED_CELL_AND_FOCUS_NEXT),
mergeMap(action => {
const contentRef = action.payload.contentRef;
return concat(
of(actions.executeFocusedCell({ contentRef })),
of(actions.focusNextCell({ contentRef, createCellIfUndefined: false }))
);
})
);
};
function getUserPuid(): string {
const arcadiaToken = window.dataExplorer && window.dataExplorer.arcadiaToken();
if (!arcadiaToken) {
return undefined;
}
let userPuid;
try {
const tokenPayload = decryptJWTToken(arcadiaToken);
if (tokenPayload && tokenPayload.hasOwnProperty("puid")) {
userPuid = tokenPayload.puid;
}
} catch (error) {
// ignore
}
return userPuid;
}
/**
* Close tab if mimetype not supported
* @param action$
* @param state$
*/
const closeUnsupportedMimetypesEpic = (
action$: ActionsObservable<actions.FetchContentFulfilled>,
state$: StateObservable<AppState>
): Observable<{}> => {
return action$.pipe(
ofType(actions.FETCH_CONTENT_FULFILLED),
mergeMap(action => {
const mimetype = action.payload.model.mimetype;
const explorer = window.dataExplorer;
if (explorer && !TextFile.handles(mimetype)) {
const filepath = action.payload.filepath;
// Close tab and show error message
explorer.closeNotebookTab(filepath);
const msg = `${filepath} cannot be rendered. Please download the file, in order to view it outside of Data Explorer.`;
explorer.showOkModalDialog("File cannot be rendered", msg);
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, msg);
}
return empty();
})
);
};
/**
* Close tab if file content fails to fetch not supported
* @param action$
* @param state$
*/
const closeContentFailedToFetchEpic = (
action$: ActionsObservable<actions.FetchContentFailed>,
state$: StateObservable<AppState>
): Observable<{}> => {
return action$.pipe(
ofType(actions.FETCH_CONTENT_FAILED),
mergeMap(action => {
const explorer = window.dataExplorer;
if (explorer) {
const filepath = action.payload.filepath;
// Close tab and show error message
explorer.closeNotebookTab(filepath);
const msg = `Failed to load file: ${filepath}.`;
explorer.showOkModalDialog("Failure to load", msg);
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, msg);
}
return empty();
})
);
};
export const allEpics = [
addInitialCodeCellEpic,
focusInitialCodeCellEpic,
notificationsToUserEpic,
launchWebSocketKernelEpic,
changeWebSocketKernelEpic,
acquireKernelInfoEpic,
handleKernelConnectionLostEpic,
cleanKernelOnConnectionLostEpic,
adjustLastModifiedOnSaveEpic,
executeFocusedCellAndFocusNextEpic,
closeUnsupportedMimetypesEpic,
closeContentFailedToFetchEpic,
restartWebSocketKernelEpic
];

View File

@@ -0,0 +1,35 @@
// This replicates transform loading from:
// https://github.com/nteract/nteract/blob/master/applications/jupyter-extension/nteract_on_jupyter/app/contents/notebook.tsx
export default (props: { addTransform: (component: any) => void }) => {
import(/* webpackChunkName: "plotly" */ "@nteract/transform-plotly").then(module => {
props.addTransform(module.default);
props.addTransform(module.PlotlyNullTransform);
});
import(/* webpackChunkName: "tabular-dataresource" */ "@nteract/data-explorer").then(module => {
props.addTransform(module.default);
});
import(/* webpackChunkName: "jupyter-widgets" */ "@nteract/jupyter-widgets").then(module => {
props.addTransform(module.WidgetDisplay);
});
import("@nteract/transform-model-debug").then(module => {
props.addTransform(module.default);
});
import(/* webpackChunkName: "vega-transform" */ "@nteract/transform-vega").then(module => {
props.addTransform(module.VegaLite1);
props.addTransform(module.VegaLite2);
props.addTransform(module.VegaLite3);
props.addTransform(module.VegaLite4);
props.addTransform(module.Vega2);
props.addTransform(module.Vega3);
props.addTransform(module.Vega4);
props.addTransform(module.Vega5);
});
// TODO: The geojson transform will likely need some work because of the basemap URL(s)
// import GeoJSONTransform from "@nteract/transform-geojson";
};

View File

@@ -0,0 +1,80 @@
import * as cdbActions from "./actions";
import { CdbRecord } from "./types";
import { Action } from "redux";
import { actions, CoreRecord, reducers as nteractReducers } from "@nteract/core";
export const coreReducer = (state: CoreRecord, action: Action) => {
let typedAction;
switch (action.type) {
case cdbActions.CLOSE_NOTEBOOK: {
typedAction = action as cdbActions.CloseNotebookAction;
return state.setIn(
["entities", "contents", "byRef"],
state.entities.contents.byRef.delete(typedAction.payload.contentRef)
);
}
case actions.CHANGE_KERNEL_BY_NAME: {
// Update content metadata
typedAction = action as actions.ChangeKernelByName;
const kernelSpecName = typedAction.payload.kernelSpecName;
if (!state.currentKernelspecsRef) {
return state;
}
const currentKernelspecs = state.entities.kernelspecs.byRef.get(state.currentKernelspecsRef);
if (!currentKernelspecs) {
return state;
}
// Find kernelspecs by name
const kernelspecs = currentKernelspecs.byName.get(kernelSpecName);
if (!kernelspecs) {
return state;
}
const path = [
"entities",
"contents",
"byRef",
typedAction.payload.contentRef,
"model",
"notebook",
"metadata",
"kernelspec"
];
// Update metadata
return state
.setIn(path.concat("name"), kernelspecs.name)
.setIn(path.concat("displayName"), kernelspecs.displayName)
.setIn(path.concat("language"), kernelspecs.language);
}
case cdbActions.UPDATE_LAST_MODIFIED: {
typedAction = action as cdbActions.UpdateLastModifiedAction;
const path = ["entities", "contents", "byRef", typedAction.payload.contentRef, "lastSaved"];
return state.setIn(path, typedAction.payload.lastModified);
}
default:
return nteractReducers.core(state as any, action as any);
}
};
export const cdbReducer = (state: CdbRecord, action: Action) => {
if (!state) {
return null;
}
switch (action.type) {
case cdbActions.UPDATE_KERNEL_RESTART_DELAY: {
const typedAction = action as cdbActions.UpdateKernelRestartDelayAction;
return state.set("kernelRestartDelayMs", typedAction.payload.delayMs);
}
case cdbActions.SET_HOVERED_CELL: {
const typedAction = action as cdbActions.SetHoveredCellAction;
return state.set("hoveredCellId", typedAction.payload.cellId);
}
}
return state;
};

View File

@@ -0,0 +1,112 @@
import { AppState, epics as coreEpics, reducers, IContentProvider } from "@nteract/core";
import {
applyMiddleware,
combineReducers,
compose,
createStore,
Store,
AnyAction,
Middleware,
Dispatch,
MiddlewareAPI
} from "redux";
import { combineEpics, createEpicMiddleware, Epic, ActionsObservable } from "redux-observable";
import { allEpics } from "./epics";
import { coreReducer, cdbReducer } from "./reducers";
import { catchError } from "rxjs/operators";
const composeEnhancers = (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
export default function configureStore(
initialState: Partial<AppState>,
contentProvider: IContentProvider,
onTraceFailure: (title: string, message: string) => void,
customMiddlewares?: Middleware<{}, any, Dispatch<AnyAction>>[]
): Store<AppState, AnyAction> {
const rootReducer = combineReducers({
app: reducers.app,
comms: reducers.comms,
config: reducers.config,
core: coreReducer,
cdb: cdbReducer
});
/**
* Catches errors in reducers
*/
const catchErrorMiddleware: Middleware = <D extends Dispatch<AnyAction>, S extends AppState>({
dispatch,
getState
}: MiddlewareAPI<D, S>) => (next: Dispatch<AnyAction>) => <A extends AnyAction>(action: A): any => {
try {
next(action);
} catch (error) {
traceFailure("Reducer failure", error);
}
};
const protect = (epic: Epic) => {
return (action$: ActionsObservable<any>, state$: any, dependencies: any) =>
epic(action$, state$, dependencies).pipe(
catchError((error, caught) => {
traceFailure("Epic failure", error);
return caught;
})
);
};
const traceFailure = (title: string, error: any) => {
if (error instanceof Error) {
onTraceFailure(title, `${error.message} ${JSON.stringify(error.stack)}`);
console.error(error);
} else {
onTraceFailure(title, JSON.stringify(error));
}
};
const combineAndProtectEpics = (epics: Epic[]): Epic => {
const protectedEpics = epics.map(epic => protect(epic));
return combineEpics<Epic>(...protectedEpics);
};
// This list needs to be consistent and in sync with core.allEpics until we figure
// out how to safely filter out the ones we are overriding here.
const filteredCoreEpics = [
coreEpics.autoSaveCurrentContentEpic,
coreEpics.executeCellEpic,
coreEpics.executeFocusedCellEpic,
coreEpics.executeCellAfterKernelLaunchEpic,
coreEpics.sendExecuteRequestEpic,
coreEpics.updateDisplayEpic,
coreEpics.executeAllCellsEpic,
coreEpics.commListenEpic,
coreEpics.interruptKernelEpic,
coreEpics.lazyLaunchKernelEpic,
coreEpics.killKernelEpic,
coreEpics.watchExecutionStateEpic,
coreEpics.restartKernelEpic,
coreEpics.fetchKernelspecsEpic,
coreEpics.fetchContentEpic,
coreEpics.updateContentEpic,
coreEpics.saveContentEpic,
coreEpics.publishToBookstore,
coreEpics.publishToBookstoreAfterSave,
coreEpics.sendInputReplyEpic
];
const rootEpic = combineAndProtectEpics([...filteredCoreEpics, ...allEpics]);
const epicMiddleware = createEpicMiddleware({ dependencies: { contentProvider } });
let middlewares: Middleware[] = [epicMiddleware];
// TODO: tamitta: errorMiddleware was removed, do we need a substitute?
if (customMiddlewares) {
middlewares = middlewares.concat(customMiddlewares);
}
middlewares.push(catchErrorMiddleware);
const store = createStore(rootReducer, initialState, composeEnhancers(applyMiddleware(...middlewares)));
epicMiddleware.run(rootEpic);
// TODO Fix typing issue here: createStore() output type doesn't quite match AppState
return store as Store<AppState, AnyAction>;
}

View File

@@ -0,0 +1,25 @@
import * as Immutable from "immutable";
import { AppState } from "@nteract/core";
import { Notebook } from "../../../Common/Constants";
import { CellId } from "@nteract/commutable";
export interface CdbRecordProps {
databaseAccountName: string;
defaultExperience: string;
kernelRestartDelayMs: number;
hoveredCellId: CellId;
}
export type CdbRecord = Immutable.RecordOf<CdbRecordProps>;
export interface CdbAppState extends AppState {
cdb: CdbRecord;
}
export const makeCdbRecord = Immutable.Record<CdbRecordProps>({
databaseAccountName: undefined,
defaultExperience: undefined,
kernelRestartDelayMs: Notebook.kernelRestartInitialDelayMs,
hoveredCellId: undefined
});