Skip to content

Commit

Permalink
feat: add document editor for .conda, .conda.yml and .yml
Browse files Browse the repository at this point in the history
and make look and feel more native.
  • Loading branch information
mariobuikhuizen committed Jun 8, 2021
1 parent 86e7334 commit 2c81ae4
Show file tree
Hide file tree
Showing 7 changed files with 553 additions and 197 deletions.
172 changes: 130 additions & 42 deletions packages/common/src/components/CondaEnvSolve.tsx
@@ -1,8 +1,10 @@
import * as React from 'react';
import CodeMirror from 'codemirror';
import CodeMirror, { Editor } from 'codemirror';
import 'codemirror/lib/codemirror.css';
import './yaml';
import * as condaHint from './CondaHint';
import { IEnvironmentManager } from '../tokens';
import { INotification } from 'jupyterlab_toastify';

/**
* Conda solve properties
Expand All @@ -11,59 +13,145 @@ export interface ICondaEnvSolveProps {
quetzUrl: string;
quetzSolverUrl: string;
subdir: string;
create(name: string, explicitList: string): void;
content?: string;
expandChannelUrl?: boolean;
onContentChange?(content: string): void;
}

export const CondaEnvSolve = (props: ICondaEnvSolveProps): JSX.Element => {
condaHint.register(props.quetzUrl);

const codemirrorElem = React.useRef();
condaHint.register(props.quetzUrl, props.expandChannelUrl);
const codemirrorElem = React.useRef(null);

const [editor, setEditor] = React.useState(null);
const [solveState, setSolveState] = React.useState(null);

async function solve() {
const environment_yml = editor.getValue();
setSolveState('Solving...');
const name = condaHint.getName(environment_yml);
try {
const solveResult = await condaHint.fetchSolve(
props.quetzUrl,
props.quetzSolverUrl,
props.subdir,
environment_yml
);
setSolveState(`Creating environment ${name}...`);
await props.create(name, solveResult);
setSolveState('Ok');
} catch (e) {
setSolveState(`Error: ${e}`);
}
}

React.useEffect(() => {
if (editor) {
if (props.content !== undefined && props.content !== editor.getValue()) {
editor.setValue(props.content);
editor.refresh();
}
return;
}
setEditor(
CodeMirror(codemirrorElem.current, {
lineNumbers: true,
extraKeys: {
'Ctrl-Space': 'autocomplete',
'Ctrl-Tab': 'autocomplete'
},
tabSize: 2,
mode: 'yaml',
autofocus: true
})
);
const newEditor = CodeMirror(codemirrorElem.current, {
value: props.content || '',
lineNumbers: true,
extraKeys: {
'Ctrl-Space': 'autocomplete',
'Ctrl-Tab': 'autocomplete'
},
tabSize: 2,
mode: 'yaml',
autofocus: true
});
if (props.onContentChange) {
newEditor.on('change', (instance: Editor) =>
props.onContentChange(instance.getValue())
);
}
setEditor(newEditor);

/* Apply lab styles to this codemirror instance */
codemirrorElem.current.childNodes[0].classList.add('cm-s-jupyter');
});
return (
<div style={{ width: '80vw', maxWidth: '900px' }}>
<div ref={codemirrorElem}></div>
<div style={{ paddingTop: '8px' }}>
<button onClick={solve}>Create</button>
<span style={{ marginLeft: '16px' }}>{solveState}</span>
<div
ref={codemirrorElem}
className="conda-complete-panel"
onMouseEnter={() => editor && editor.refresh()}
/>
);
};

export async function solveAndCreateEnvironment(
environment_yml: string,
environmentManager: IEnvironmentManager,
expandChannelUrl: boolean,
onMessage?: (msg: string) => void
): Promise<void> {
const name = condaHint.getName(environment_yml);
const { quetzUrl, quetzSolverUrl, subdir } = environmentManager;

let message = 'Solving environment...';
onMessage && onMessage(message);
let toastId = await INotification.inProgress(message);
try {
const explicitList = await condaHint.fetchSolve(
quetzUrl,
quetzSolverUrl,
(await subdir()).subdir,
environment_yml,
expandChannelUrl
);
await INotification.update({
toastId,
message: 'Environment has been solved.',
type: 'success',
autoClose: 5000
});

message = `creating environment ${name}...`;
onMessage && onMessage(message);
toastId = await INotification.inProgress(message);
await environmentManager.import(name, explicitList);

message = `Environment ${name} created.`;
onMessage && onMessage(message);
await INotification.update({
toastId,
message,
type: 'success',
autoClose: 5000
});
} catch (error) {
onMessage && onMessage(error.message);
if (toastId) {
await INotification.update({
toastId,
message: error.message,
type: 'error',
autoClose: 0
});
}
}
}

export interface ICondaEnvSolveDialogProps {
subdir: string;
environmentManager: IEnvironmentManager;
}

export const CondaEnvSolveDialog = (
props: ICondaEnvSolveDialogProps
): JSX.Element => {
const [environment_yml, setEnvironment_yml] = React.useState('');
const [solveState, setSolveState] = React.useState(null);

return (
<div className="condaCompleteDialog__panel">
<div style={{ flexGrow: 1 }}>
<CondaEnvSolve
expandChannelUrl={false}
subdir={props.subdir}
quetzUrl={props.environmentManager.quetzUrl}
quetzSolverUrl={props.environmentManager.quetzSolverUrl}
onContentChange={setEnvironment_yml}
/>
</div>
<div style={{ padding: '12px' }}>
<button
onClick={() =>
solveAndCreateEnvironment(
environment_yml,
props.environmentManager,
true,
setSolveState
)
}
className="jp-Dialog-button jp-mod-accept jp-mod-styled"
>
Create
</button>
<span style={{ marginLeft: '12px' }}>{solveState}</span>
</div>
</div>
);
Expand Down
34 changes: 20 additions & 14 deletions packages/common/src/components/CondaHint.ts
Expand Up @@ -121,7 +121,7 @@ async function loadVersionsOfChannels(
return mapping;
}

export function register(quetzUrl: string): void {
export function register(quetzUrl: string, expandChannelUrl = false): void {
const condaHint = async (editor: Editor, callback: any, options: any) => {
const topLevel = [
{
Expand Down Expand Up @@ -173,9 +173,12 @@ export function register(quetzUrl: string): void {
const [, pre, name] = groups;
start = pre.length;
try {
list = (await loadChannels(quetzUrl)).filter(channel =>
channel.startsWith(name || '')
);
list = (await loadChannels(quetzUrl))
.filter(channel => channel.startsWith(name || ''))
.map(channel => ({
displayText: channel,
text: expandChannelUrl ? `${quetzUrl}/get/${channel}` : channel
}));
} catch (e) {
console.error(e);
list = wrapErrorMsg('Loading of channels failed');
Expand Down Expand Up @@ -229,11 +232,15 @@ export function register(quetzUrl: string): void {
version2End
] = matchLengths;

const channels = getChannels(editor.getValue()).map(channel =>
expandChannelUrl ? channel.slice(quetzUrl.length + 5) : channel
);

const loadVersions = async (query: string) => {
try {
return await loadVersionsOfChannels(
quetzUrl,
getChannels(editor.getValue()),
channels,
packageName,
query
);
Expand All @@ -253,11 +260,7 @@ export function register(quetzUrl: string): void {

try {
list = (
await loadPackagesOfChannels(
quetzUrl,
getChannels(editor.getValue()),
packagePart
)
await loadPackagesOfChannels(quetzUrl, channels, packagePart)
).map(p => ({
displayText: `${p.name} [${p.channel}] ${p.summary}`,
text: `${p.name}`
Expand Down Expand Up @@ -304,13 +307,16 @@ export async function fetchSolve(
quetzUrl: string,
quetzSolverUrl: string,
subdir: string,
environment_yml: string
environment_yml: string,
expandChannelUrl = true
): Promise<string> {
const data = {
subdir,
channels: getChannels(environment_yml).map(
channel => `${quetzUrl}/get/${channel}`
),
channels: expandChannelUrl
? getChannels(environment_yml).map(
channel => `${quetzUrl}/get/${channel}`
)
: getChannels(environment_yml),
spec: getDependencies(environment_yml)
};
const response = await fetch(
Expand Down
52 changes: 8 additions & 44 deletions packages/common/src/components/NbConda.tsx
Expand Up @@ -6,7 +6,7 @@ import { style } from 'typestyle';
import { Conda, IEnvironmentManager } from '../tokens';
import { CondaEnvList, ENVIRONMENT_PANEL_WIDTH } from './CondaEnvList';
import { CondaPkgPanel } from './CondaPkgPanel';
import { CondaEnvSolve } from './CondaEnvSolve';
import { CondaEnvSolveDialog } from './CondaEnvSolve';

/**
* Jupyter Conda Component properties
Expand Down Expand Up @@ -365,52 +365,16 @@ export class NbConda extends React.Component<ICondaEnvProps, ICondaEnvState> {
}

async handleSolveEnvironment(): Promise<void> {
let toastId: React.ReactText;
const model = this.props.model;
const { subdir } = await model.subdir();

const create = async (name: string, explicitList: string) => {
toastId = await INotification.inProgress(`Creating environment ${name}`);
try {
await model.import(name, explicitList);
INotification.update({
toastId,
message: `Environment ${name} has been created.`,
type: 'success',
autoClose: 5000
});
this.setState({ currentEnvironment: name });
this.loadEnvironments();
} catch (error) {
if (error !== 'cancelled') {
console.error(error);
if (toastId) {
INotification.update({
toastId,
message: error.message,
type: 'error',
autoClose: 0
});
} else {
INotification.error(error.message);
}
} else {
if (toastId) {
INotification.dismiss(toastId);
}
}
throw error;
}
};
const { subdir } = await this.props.model.subdir();
const dialog = new NonKeypressStealingDialog({
title: 'Solve Environment',
body: (
<CondaEnvSolve
quetzUrl={model.quetzUrl}
quetzSolverUrl={model.quetzSolverUrl}
subdir={subdir}
create={create}
/>
<div className="condaCompleteDialog">
<CondaEnvSolveDialog
subdir={subdir}
environmentManager={this.props.model}
/>
</div>
),
buttons: [Dialog.okButton()]
});
Expand Down

0 comments on commit 2c81ae4

Please sign in to comment.