Skip to content

Commit 7ddd2b5

Browse files
fix: improve ipynb loading hitches (#132)
* fix: improve ipynb loading hitches this fixes three issues that happen when loading an ipynb file in the code editor: 1) when an ipynb file was selected and the file content wasn't loaded, the error handler was called 2) when a parse error occurred, the error handler was called with incorrect arguments 3) when loading an ipynb file, the unparsed file content renders to the page briefly even when the content is valid We also update the loadable usage in the codeeditor to use the loadable class. * 0.6.42-ET741.0 * fix filename container shrinking during load * 0.6.42-ET741.1 * add comments --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
1 parent 8bc776f commit 7ddd2b5

File tree

5 files changed

+101
-94
lines changed

5 files changed

+101
-94
lines changed

package-lock.json

+2-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@hpe.com/hew",
3-
"version": "0.6.41",
3+
"version": "0.6.42-ET741.1",
44
"type": "module",
55
"scripts": {
66
"dev": "vite",

src/kit/CodeEditor.tsx

+38-35
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import Message from 'kit/Message';
1313
import Spinner from 'kit/Spinner';
1414
import { useTheme } from 'kit/Theme';
1515
import { ErrorHandler } from 'kit/utils/error';
16-
import { Loadable, Loaded, NotLoaded } from 'kit/utils/loadable';
16+
import { Loadable } from 'kit/utils/loadable';
1717
import { TreeNode, ValueOf } from 'kit/utils/types';
1818

1919
const JupyterRenderer = lazy(() => import('./CodeEditor/IpynbRenderer'));
@@ -100,6 +100,10 @@ const langs = {
100100
yaml: () => StreamLanguage.define(yaml),
101101
};
102102

103+
const emptyIpynbFile = JSON.stringify({
104+
cells: [],
105+
});
106+
103107
/**
104108
* A component responsible to enable the user to view the code for a experiment.
105109
*
@@ -126,7 +130,7 @@ const CodeEditor: React.FC<Props> = ({
126130
readonly,
127131
selectedFilePath = String(files[0]?.key),
128132
}) => {
129-
const loadableFile = useMemo(() => (typeof file === 'string' ? Loaded(file) : file), [file]);
133+
const loadableFile = useMemo(() => Loadable.ensureLoadable(file), [file]);
130134
const sortedFiles = useMemo(() => [...files].sort(sortTree), [files]);
131135
const {
132136
themeSettings: { themeIsDark, className: themeClass },
@@ -193,8 +197,7 @@ const CodeEditor: React.FC<Props> = ({
193197
);
194198

195199
const handleDownloadClick = useCallback(() => {
196-
if (!Loadable.isLoadable(loadableFile) || !Loadable.isLoaded(loadableFile) || !activeFile)
197-
return;
200+
if (!loadableFile.isLoaded || !activeFile) return;
198201

199202
const link = document.createElement('a');
200203

@@ -215,7 +218,7 @@ const CodeEditor: React.FC<Props> = ({
215218
themeClass,
216219
];
217220

218-
const sectionClasses = [loadableFile.isFailed ? css.pageError : css.editor];
221+
const sectionClass = loadableFile.isFailed ? css.pageError : css.editor;
219222

220223
const treeClasses = [css.fileTree, viewMode === 'editor' ? css.hideElement : ''];
221224

@@ -237,7 +240,10 @@ const CodeEditor: React.FC<Props> = ({
237240
/>
238241
) : (
239242
<Suspense fallback={<Spinner spinning tip="Loading ipynb viewer..." />}>
240-
<JupyterRenderer file={Loadable.getOrElse('', loadableFile)} onError={onError} />
243+
<JupyterRenderer
244+
file={Loadable.getOrElse(emptyIpynbFile, loadableFile)}
245+
onError={onError}
246+
/>
241247
</Suspense>
242248
);
243249
}
@@ -253,39 +259,36 @@ const CodeEditor: React.FC<Props> = ({
253259
onSelect={handleSelectFile}
254260
/>
255261
{!!activeFile?.title && (
256-
<div className={css.fileDir}>
257-
<div className={css.fileInfo}>
258-
<div className={css.buttonContainer}>
259-
<>
260-
{activeFile.icon ?? <Icon decorative name="document" />}
261-
<span className={css.filePath}>
262-
<>{activeFile.title}</>
263-
</span>
264-
{activeFile?.subtitle && (
265-
<span className={css.fileDesc}> {activeFile?.subtitle}</span>
266-
)}
267-
{readonly && <span className={css.readOnly}>read-only</span>}
268-
</>
269-
</div>
270-
<div className={css.buttonsContainer}>
271-
{/*
272-
* TODO: Add notebook integration
273-
* <Button type="text">Open in Notebook</Button>
274-
*/}
275-
{readonly && file !== NotLoaded && (
276-
<Button
277-
icon={<Icon name="download" showTooltip size="small" title="Download File" />}
278-
type="text"
279-
onClick={handleDownloadClick}
280-
/>
262+
<div className={css.fileInfo}>
263+
<div className={css.buttonContainer}>
264+
<>
265+
{activeFile.icon ?? <Icon decorative name="document" />}
266+
<span className={css.filePath}>
267+
<>{activeFile.title}</>
268+
</span>
269+
{activeFile?.subtitle && (
270+
<span className={css.fileDesc}> {activeFile?.subtitle}</span>
281271
)}
282-
</div>
272+
{readonly && <span className={css.readOnly}>read-only</span>}
273+
</>
274+
</div>
275+
<div className={css.buttonsContainer}>
276+
{/*
277+
* TODO: Add notebook integration
278+
* <Button type="text">Open in Notebook</Button>
279+
*/}
280+
{readonly && !loadableFile.isNotLoaded && (
281+
<Button
282+
icon={<Icon name="download" showTooltip size="small" title="Download File" />}
283+
type="text"
284+
onClick={handleDownloadClick}
285+
/>
286+
)}
283287
</div>
284288
</div>
285289
)}
286-
<div className={sectionClasses.join(' ')}>
287-
{/* directly checking tag because loadable.isLoaded only takes loadables */}
288-
<Spinner spinning={file === NotLoaded}>{fileContent}</Spinner>
290+
<div className={sectionClass}>
291+
<Spinner spinning={loadableFile.isNotLoaded}>{fileContent}</Spinner>
289292
</div>
290293
</div>
291294
);

src/kit/CodeEditor/CodeEditor.module.scss

+25-28
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
.codeEditorBase {
22
display: grid;
33
grid-template:
4-
'tree title' minmax(38px, min-content)
4+
'tree title' minmax(40px, min-content)
55
'tree editor' auto / clamp(300px, 25vw, 400px) minmax(0, auto);
66
max-width: 100vw;
77
min-height: 250px;
@@ -27,38 +27,35 @@
2727
margin-top: 5em;
2828
text-align: center;
2929
}
30-
.fileDir {
30+
.fileInfo {
31+
align-items: center;
32+
background-color: var(--theme-stage);
33+
border: solid var(--theme-stroke-width) var(--theme-stage-border);
34+
border-bottom: none;
35+
border-radius: 0;
36+
display: flex;
3137
grid-area: title;
38+
justify-content: space-between;
39+
padding: 0.25em 1em;
3240

33-
.fileInfo {
41+
.buttonContainer {
3442
align-items: center;
35-
background-color: var(--theme-stage);
36-
border: solid var(--theme-stroke-width) var(--theme-stage-border);
37-
border-bottom: none;
38-
border-radius: 0;
3943
display: flex;
4044
justify-content: space-between;
41-
padding: 0.25em 1em;
42-
43-
.buttonContainer {
44-
align-items: center;
45-
display: flex;
46-
justify-content: space-between;
47-
}
48-
.filePath {
49-
margin-left: 10px;
50-
}
51-
.fileDesc,
52-
.readOnly {
53-
color: var(--theme-stage-on-weak);
54-
margin-left: var(--spacing-xl-2);
55-
}
56-
.readOnly {
57-
font-variant: small-caps;
58-
}
59-
.buttonsContainer {
60-
display: flex;
61-
}
45+
}
46+
.filePath {
47+
margin-left: 10px;
48+
}
49+
.fileDesc,
50+
.readOnly {
51+
color: var(--theme-stage-on-weak);
52+
margin-left: var(--spacing-xl-2);
53+
}
54+
.readOnly {
55+
font-variant: small-caps;
56+
}
57+
.buttonsContainer {
58+
display: flex;
6259
}
6360
}
6461
.editor {

src/kit/CodeEditor/IpynbRenderer.tsx

+35-28
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
import React, { useEffect, useState } from 'react';
1+
import { mapLeft, match, tryCatch } from 'fp-ts/Either';
2+
import { pipe } from 'fp-ts/function';
3+
import React, { useEffect, useMemo } from 'react';
24

35
import { ErrorHandler, ErrorType } from 'kit/utils/error';
46
import NotebookJS from 'notebook';
@@ -10,38 +12,43 @@ interface Props {
1012
onError: ErrorHandler;
1113
}
1214

13-
export const parseNotebook = (file: string, onError: ErrorHandler): string => {
14-
try {
15-
const json = JSON.parse(file);
16-
const notebookJS = NotebookJS.parse(json);
17-
return notebookJS.render().outerHTML;
18-
} catch (e) {
19-
onError('Unable to parse as Notebook!');
20-
return '';
21-
}
15+
export const parseNotebook = (file: string): string => {
16+
const json = JSON.parse(file);
17+
const notebookJS = NotebookJS.parse(json);
18+
return notebookJS.render().outerHTML;
2219
};
2320

2421
const JupyterRenderer: React.FC<Props> = React.memo(({ file, onError }) => {
25-
const [__html, setHTML] = useState<string>();
22+
// parse the file and store the result as either a successful string or a failed error
23+
const parseResult = useMemo(() => {
24+
return tryCatch(
25+
() => parseNotebook(file),
26+
(e) => e,
27+
);
28+
}, [file]);
2629

2730
useEffect(() => {
28-
try {
29-
const html = parseNotebook(file, onError);
30-
setHTML(html);
31-
} catch (error) {
32-
onError(error, {
33-
publicMessage: 'Failed to load selected notebook.',
34-
publicSubject: 'Unable to parse the selected notebook.',
35-
silent: true,
36-
type: ErrorType.Input,
37-
});
38-
}
39-
}, [file, onError]);
40-
41-
return __html ? (
42-
<div className="ipynb-renderer-root" dangerouslySetInnerHTML={{ __html }} />
43-
) : (
44-
<div>{file}</div>
31+
// if the parse result is failed, call the error handler
32+
pipe(
33+
parseResult,
34+
mapLeft((e) => {
35+
onError(e, {
36+
publicMessage: 'Failed to load selected notebook.',
37+
publicSubject: 'Unable to parse the selected notebook.',
38+
silent: true,
39+
type: ErrorType.Input,
40+
});
41+
}),
42+
);
43+
}, [parseResult, onError]);
44+
45+
// if the parse result is failed, fall back to the raw file, otherwise show the parse result html
46+
return pipe(
47+
parseResult,
48+
match(
49+
() => <div>{file}</div>,
50+
(__html) => <div className="ipynb-renderer-root" dangerouslySetInnerHTML={{ __html }} />,
51+
),
4552
);
4653
});
4754

0 commit comments

Comments
 (0)