Skip to content

Commit 2c81ae4

Browse files
feat: add document editor for .conda, .conda.yml and .yml
and make look and feel more native.
1 parent 86e7334 commit 2c81ae4

7 files changed

Lines changed: 553 additions & 197 deletions

File tree

packages/common/src/components/CondaEnvSolve.tsx

Lines changed: 130 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import * as React from 'react';
2-
import CodeMirror from 'codemirror';
2+
import CodeMirror, { Editor } from 'codemirror';
33
import 'codemirror/lib/codemirror.css';
44
import './yaml';
55
import * as condaHint from './CondaHint';
6+
import { IEnvironmentManager } from '../tokens';
7+
import { INotification } from 'jupyterlab_toastify';
68

79
/**
810
* Conda solve properties
@@ -11,59 +13,145 @@ export interface ICondaEnvSolveProps {
1113
quetzUrl: string;
1214
quetzSolverUrl: string;
1315
subdir: string;
14-
create(name: string, explicitList: string): void;
16+
content?: string;
17+
expandChannelUrl?: boolean;
18+
onContentChange?(content: string): void;
1519
}
1620

1721
export const CondaEnvSolve = (props: ICondaEnvSolveProps): JSX.Element => {
18-
condaHint.register(props.quetzUrl);
19-
20-
const codemirrorElem = React.useRef();
22+
condaHint.register(props.quetzUrl, props.expandChannelUrl);
23+
const codemirrorElem = React.useRef(null);
2124

2225
const [editor, setEditor] = React.useState(null);
23-
const [solveState, setSolveState] = React.useState(null);
24-
25-
async function solve() {
26-
const environment_yml = editor.getValue();
27-
setSolveState('Solving...');
28-
const name = condaHint.getName(environment_yml);
29-
try {
30-
const solveResult = await condaHint.fetchSolve(
31-
props.quetzUrl,
32-
props.quetzSolverUrl,
33-
props.subdir,
34-
environment_yml
35-
);
36-
setSolveState(`Creating environment ${name}...`);
37-
await props.create(name, solveResult);
38-
setSolveState('Ok');
39-
} catch (e) {
40-
setSolveState(`Error: ${e}`);
41-
}
42-
}
4326

4427
React.useEffect(() => {
4528
if (editor) {
29+
if (props.content !== undefined && props.content !== editor.getValue()) {
30+
editor.setValue(props.content);
31+
editor.refresh();
32+
}
4633
return;
4734
}
48-
setEditor(
49-
CodeMirror(codemirrorElem.current, {
50-
lineNumbers: true,
51-
extraKeys: {
52-
'Ctrl-Space': 'autocomplete',
53-
'Ctrl-Tab': 'autocomplete'
54-
},
55-
tabSize: 2,
56-
mode: 'yaml',
57-
autofocus: true
58-
})
59-
);
35+
const newEditor = CodeMirror(codemirrorElem.current, {
36+
value: props.content || '',
37+
lineNumbers: true,
38+
extraKeys: {
39+
'Ctrl-Space': 'autocomplete',
40+
'Ctrl-Tab': 'autocomplete'
41+
},
42+
tabSize: 2,
43+
mode: 'yaml',
44+
autofocus: true
45+
});
46+
if (props.onContentChange) {
47+
newEditor.on('change', (instance: Editor) =>
48+
props.onContentChange(instance.getValue())
49+
);
50+
}
51+
setEditor(newEditor);
52+
53+
/* Apply lab styles to this codemirror instance */
54+
codemirrorElem.current.childNodes[0].classList.add('cm-s-jupyter');
6055
});
6156
return (
62-
<div style={{ width: '80vw', maxWidth: '900px' }}>
63-
<div ref={codemirrorElem}></div>
64-
<div style={{ paddingTop: '8px' }}>
65-
<button onClick={solve}>Create</button>
66-
<span style={{ marginLeft: '16px' }}>{solveState}</span>
57+
<div
58+
ref={codemirrorElem}
59+
className="conda-complete-panel"
60+
onMouseEnter={() => editor && editor.refresh()}
61+
/>
62+
);
63+
};
64+
65+
export async function solveAndCreateEnvironment(
66+
environment_yml: string,
67+
environmentManager: IEnvironmentManager,
68+
expandChannelUrl: boolean,
69+
onMessage?: (msg: string) => void
70+
): Promise<void> {
71+
const name = condaHint.getName(environment_yml);
72+
const { quetzUrl, quetzSolverUrl, subdir } = environmentManager;
73+
74+
let message = 'Solving environment...';
75+
onMessage && onMessage(message);
76+
let toastId = await INotification.inProgress(message);
77+
try {
78+
const explicitList = await condaHint.fetchSolve(
79+
quetzUrl,
80+
quetzSolverUrl,
81+
(await subdir()).subdir,
82+
environment_yml,
83+
expandChannelUrl
84+
);
85+
await INotification.update({
86+
toastId,
87+
message: 'Environment has been solved.',
88+
type: 'success',
89+
autoClose: 5000
90+
});
91+
92+
message = `creating environment ${name}...`;
93+
onMessage && onMessage(message);
94+
toastId = await INotification.inProgress(message);
95+
await environmentManager.import(name, explicitList);
96+
97+
message = `Environment ${name} created.`;
98+
onMessage && onMessage(message);
99+
await INotification.update({
100+
toastId,
101+
message,
102+
type: 'success',
103+
autoClose: 5000
104+
});
105+
} catch (error) {
106+
onMessage && onMessage(error.message);
107+
if (toastId) {
108+
await INotification.update({
109+
toastId,
110+
message: error.message,
111+
type: 'error',
112+
autoClose: 0
113+
});
114+
}
115+
}
116+
}
117+
118+
export interface ICondaEnvSolveDialogProps {
119+
subdir: string;
120+
environmentManager: IEnvironmentManager;
121+
}
122+
123+
export const CondaEnvSolveDialog = (
124+
props: ICondaEnvSolveDialogProps
125+
): JSX.Element => {
126+
const [environment_yml, setEnvironment_yml] = React.useState('');
127+
const [solveState, setSolveState] = React.useState(null);
128+
129+
return (
130+
<div className="condaCompleteDialog__panel">
131+
<div style={{ flexGrow: 1 }}>
132+
<CondaEnvSolve
133+
expandChannelUrl={false}
134+
subdir={props.subdir}
135+
quetzUrl={props.environmentManager.quetzUrl}
136+
quetzSolverUrl={props.environmentManager.quetzSolverUrl}
137+
onContentChange={setEnvironment_yml}
138+
/>
139+
</div>
140+
<div style={{ padding: '12px' }}>
141+
<button
142+
onClick={() =>
143+
solveAndCreateEnvironment(
144+
environment_yml,
145+
props.environmentManager,
146+
true,
147+
setSolveState
148+
)
149+
}
150+
className="jp-Dialog-button jp-mod-accept jp-mod-styled"
151+
>
152+
Create
153+
</button>
154+
<span style={{ marginLeft: '12px' }}>{solveState}</span>
67155
</div>
68156
</div>
69157
);

packages/common/src/components/CondaHint.ts

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ async function loadVersionsOfChannels(
121121
return mapping;
122122
}
123123

124-
export function register(quetzUrl: string): void {
124+
export function register(quetzUrl: string, expandChannelUrl = false): void {
125125
const condaHint = async (editor: Editor, callback: any, options: any) => {
126126
const topLevel = [
127127
{
@@ -173,9 +173,12 @@ export function register(quetzUrl: string): void {
173173
const [, pre, name] = groups;
174174
start = pre.length;
175175
try {
176-
list = (await loadChannels(quetzUrl)).filter(channel =>
177-
channel.startsWith(name || '')
178-
);
176+
list = (await loadChannels(quetzUrl))
177+
.filter(channel => channel.startsWith(name || ''))
178+
.map(channel => ({
179+
displayText: channel,
180+
text: expandChannelUrl ? `${quetzUrl}/get/${channel}` : channel
181+
}));
179182
} catch (e) {
180183
console.error(e);
181184
list = wrapErrorMsg('Loading of channels failed');
@@ -229,11 +232,15 @@ export function register(quetzUrl: string): void {
229232
version2End
230233
] = matchLengths;
231234

235+
const channels = getChannels(editor.getValue()).map(channel =>
236+
expandChannelUrl ? channel.slice(quetzUrl.length + 5) : channel
237+
);
238+
232239
const loadVersions = async (query: string) => {
233240
try {
234241
return await loadVersionsOfChannels(
235242
quetzUrl,
236-
getChannels(editor.getValue()),
243+
channels,
237244
packageName,
238245
query
239246
);
@@ -253,11 +260,7 @@ export function register(quetzUrl: string): void {
253260

254261
try {
255262
list = (
256-
await loadPackagesOfChannels(
257-
quetzUrl,
258-
getChannels(editor.getValue()),
259-
packagePart
260-
)
263+
await loadPackagesOfChannels(quetzUrl, channels, packagePart)
261264
).map(p => ({
262265
displayText: `${p.name} [${p.channel}] ${p.summary}`,
263266
text: `${p.name}`
@@ -304,13 +307,16 @@ export async function fetchSolve(
304307
quetzUrl: string,
305308
quetzSolverUrl: string,
306309
subdir: string,
307-
environment_yml: string
310+
environment_yml: string,
311+
expandChannelUrl = true
308312
): Promise<string> {
309313
const data = {
310314
subdir,
311-
channels: getChannels(environment_yml).map(
312-
channel => `${quetzUrl}/get/${channel}`
313-
),
315+
channels: expandChannelUrl
316+
? getChannels(environment_yml).map(
317+
channel => `${quetzUrl}/get/${channel}`
318+
)
319+
: getChannels(environment_yml),
314320
spec: getDependencies(environment_yml)
315321
};
316322
const response = await fetch(

packages/common/src/components/NbConda.tsx

Lines changed: 8 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { style } from 'typestyle';
66
import { Conda, IEnvironmentManager } from '../tokens';
77
import { CondaEnvList, ENVIRONMENT_PANEL_WIDTH } from './CondaEnvList';
88
import { CondaPkgPanel } from './CondaPkgPanel';
9-
import { CondaEnvSolve } from './CondaEnvSolve';
9+
import { CondaEnvSolveDialog } from './CondaEnvSolve';
1010

1111
/**
1212
* Jupyter Conda Component properties
@@ -365,52 +365,16 @@ export class NbConda extends React.Component<ICondaEnvProps, ICondaEnvState> {
365365
}
366366

367367
async handleSolveEnvironment(): Promise<void> {
368-
let toastId: React.ReactText;
369-
const model = this.props.model;
370-
const { subdir } = await model.subdir();
371-
372-
const create = async (name: string, explicitList: string) => {
373-
toastId = await INotification.inProgress(`Creating environment ${name}`);
374-
try {
375-
await model.import(name, explicitList);
376-
INotification.update({
377-
toastId,
378-
message: `Environment ${name} has been created.`,
379-
type: 'success',
380-
autoClose: 5000
381-
});
382-
this.setState({ currentEnvironment: name });
383-
this.loadEnvironments();
384-
} catch (error) {
385-
if (error !== 'cancelled') {
386-
console.error(error);
387-
if (toastId) {
388-
INotification.update({
389-
toastId,
390-
message: error.message,
391-
type: 'error',
392-
autoClose: 0
393-
});
394-
} else {
395-
INotification.error(error.message);
396-
}
397-
} else {
398-
if (toastId) {
399-
INotification.dismiss(toastId);
400-
}
401-
}
402-
throw error;
403-
}
404-
};
368+
const { subdir } = await this.props.model.subdir();
405369
const dialog = new NonKeypressStealingDialog({
406370
title: 'Solve Environment',
407371
body: (
408-
<CondaEnvSolve
409-
quetzUrl={model.quetzUrl}
410-
quetzSolverUrl={model.quetzSolverUrl}
411-
subdir={subdir}
412-
create={create}
413-
/>
372+
<div className="condaCompleteDialog">
373+
<CondaEnvSolveDialog
374+
subdir={subdir}
375+
environmentManager={this.props.model}
376+
/>
377+
</div>
414378
),
415379
buttons: [Dialog.okButton()]
416380
});

0 commit comments

Comments
 (0)