Skip to content

Commit 574b3db

Browse files
committed
dialog prompt for new clusters
specify profile, cluster id, n doesn't yet expose things like launcher class, etc.
1 parent 1ea8135 commit 574b3db

File tree

5 files changed

+253
-179
lines changed

5 files changed

+253
-179
lines changed

ipyparallel/nbextension/handlers.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,24 @@ def sort_key(model):
9292
@web.authenticated
9393
def post(self):
9494
body = self.get_json_body() or {}
95+
self.log.info(f"Creating cluster with {body}")
96+
# clear null values
97+
for key in ('profile', 'cluster_id', 'n'):
98+
if key in body and body[key] in {None, ''}:
99+
body.pop(key)
100+
if 'profile' in body and os.path.sep in body['profile']:
101+
# if it looks like a path, use profile_dir
102+
body['profile_dir'] = body.pop('profile')
103+
104+
if (
105+
'profile' in body
106+
and 'cluster_id' not in body
107+
and f"{body['profile']}:" not in self.cluster_mananger._clusters
108+
):
109+
# if no cluster exists for a profile,
110+
# default for no cluster id instead of random
111+
body["cluster_id"] = ""
112+
95113
cluster_id, cluster = self.cluster_manager.new_cluster(**body)
96114
self.write(json.dumps(self.cluster_model(cluster_id, cluster)))
97115

lab/src/clusters.tsx

Lines changed: 33 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import { ISignal, Signal } from "@lumino/signaling";
2929

3030
import { Widget, PanelLayout } from "@lumino/widgets";
3131

32-
import { showScalingDialog } from "./scaling";
32+
import { newClusterDialog, INewCluster } from "./dialog";
3333

3434
import * as React from "react";
3535
import * as ReactDOM from "react-dom";
@@ -189,7 +189,11 @@ export class ClusterManager extends Widget {
189189
* Create a new cluster.
190190
*/
191191
async create(): Promise<IClusterModel> {
192-
const cluster = await this._newCluster();
192+
const clusterRequest = await newClusterDialog({});
193+
if (!clusterRequest) {
194+
return;
195+
}
196+
const cluster = await this._newCluster(clusterRequest);
193197
return cluster;
194198
}
195199

@@ -444,18 +448,20 @@ export class ClusterManager extends Widget {
444448
/**
445449
* Launch a new cluster on the server.
446450
*/
447-
private async _newCluster(): Promise<IClusterModel> {
451+
private async _newCluster(
452+
clusterRequest: INewCluster
453+
): Promise<IClusterModel> {
448454
this._isReady = false;
449455
this._registry.notifyCommandChanged(CommandIDs.newCluster);
450456
// TODO: allow requesting a profile, options
451457
const response = await ServerConnection.makeRequest(
452458
`${this._serverSettings.baseUrl}${CLUSTER_PREFIX}`,
453-
{ method: "POST" },
459+
{ method: "POST", body: JSON.stringify(clusterRequest) },
454460
this._serverSettings
455461
);
456462
if (response.status !== 200) {
457463
const err = await response.json();
458-
void showErrorMessage("Cluster Start Error", err);
464+
void showErrorMessage("Cluster Create Error", err);
459465
this._isReady = true;
460466
this._registry.notifyCommandChanged(CommandIDs.newCluster);
461467
throw err;
@@ -544,7 +550,14 @@ export class ClusterManager extends Widget {
544550
if (!cluster) {
545551
throw Error(`Failed to find cluster ${id} to scale`);
546552
}
547-
const update = await showScalingDialog(cluster);
553+
// TODO: scale not implemented
554+
// should add an engine set
555+
void showErrorMessage("Scale not implemented", "");
556+
557+
// const update = await showScalingDialog(cluster);
558+
559+
const update = cluster;
560+
548561
if (JSONExt.deepEqual(update, cluster)) {
549562
// If the user canceled, or the model is identical don't try to update.
550563
return Promise.resolve(cluster);
@@ -706,6 +719,9 @@ function ClusterListingItem(props: IClusterListingItemProps) {
706719
cluster_state = cluster.controller.state.state;
707720
}
708721

722+
// stop action is 'delete' for already-stopped clusters
723+
let STOP = cluster_state === "Stopped" ? "DELETE" : "STOP";
724+
709725
return (
710726
<li
711727
className={itemClass}
@@ -753,15 +769,17 @@ function ClusterListingItem(props: IClusterListingItemProps) {
753769
</button>
754770
<button
755771
className={`ipp-ClusterListingItem-button ipp-ClusterListingItem-stop jp-mod-styled ${
756-
cluster_state == "Stopped" ? "ipp-hidden" : ""
772+
cluster_state === "Stopped" && cluster.cluster.cluster_id === ""
773+
? "ipp-hidden"
774+
: ""
757775
}`}
758776
onClick={async (evt) => {
759777
evt.stopPropagation();
760778
return stop();
761779
}}
762-
title={`Stop ${cluster.id}`}
780+
title={STOP}
763781
>
764-
STOP
782+
{STOP}
765783
</button>
766784
</div>
767785
</li>
@@ -807,30 +825,6 @@ export interface IClusterListingItemProps {
807825
*/
808826
injectClientCode: () => void;
809827
}
810-
{
811-
/* {'cluster': {'cluster_id': 'touchy-1627466540-zp7z',
812-
'controller_args': [],
813-
'controller_ip': '',
814-
'controller_location': '',
815-
'delay': 1.0,
816-
'n': None,
817-
'profile_dir': '/Users/minrk/.ipython/profile_default',
818-
'class': 'ipyparallel.cluster.cluster.Cluster'},
819-
'controller': {'class': 'ipyparallel.cluster.launcher.LocalControllerLauncher',
820-
'state': {'cluster_id': 'touchy-1627466540-zp7z',
821-
'output_file': '/Users/minrk/.ipython/profile_default/log/ipcontroller-touchy-1627466540-zp7z-6738.log',
822-
'pid': 6835,
823-
'profile_dir': '/Users/minrk/.ipython/profile_default',
824-
'state': 'running'}},
825-
'engines': {'class': 'ipyparallel.cluster.launcher.LocalEngineSetLauncher',
826-
'sets': {'1627466817-2jkt': {'cluster_id': 'touchy-1627466540-zp7z',
827-
'n': 4,
828-
'pid': -1,
829-
'profile_dir': '/Users/minrk/.ipython/profile_default',
830-
'state': 'running',
831-
}}}}}
832-
*/
833-
}
834828

835829
/**
836830
* An interface for a JSON-serializable representation of a cluster.
@@ -860,6 +854,12 @@ export interface IClusterModel extends JSONObject {
860854
* The profile directory
861855
*/
862856
profile_dir: string;
857+
858+
/**
859+
* The profile, abbreviated
860+
*/
861+
profle: string;
862+
863863
/**
864864
* The class import string
865865
*/

lab/src/dialog.tsx

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
import { Dialog, showDialog } from "@jupyterlab/apputils";
2+
3+
import * as React from "react";
4+
5+
export interface INewCluster {
6+
/**
7+
* The cluster id
8+
*/
9+
cluster_id?: string;
10+
/**
11+
* The profile
12+
*/
13+
profile?: string;
14+
/**
15+
* The number of engines
16+
*/
17+
n?: number;
18+
}
19+
20+
/**
21+
* A namespace for ClusterDialog statics.
22+
*/
23+
namespace NewCluster {
24+
/**
25+
* The props for the NewClusterDialog component.
26+
*/
27+
28+
export interface IProps {
29+
/**
30+
* The initial cluster model shown in the Dialog.
31+
*/
32+
initialModel: INewCluster;
33+
34+
/**
35+
* A callback that allows the component to write state to an
36+
* external object.
37+
*/
38+
stateEscapeHatch: (model: INewCluster) => void;
39+
}
40+
41+
/**
42+
* The state for the ClusterDialog component.
43+
*/
44+
export interface IState {
45+
/**
46+
* The proposed cluster model shown in the Dialog.
47+
*/
48+
model: INewCluster;
49+
}
50+
}
51+
52+
/**
53+
* A component for an HTML form that allows the user
54+
* to select Dialog parameters.
55+
*/
56+
export class NewCluster extends React.Component<
57+
NewCluster.IProps,
58+
NewCluster.IState
59+
> {
60+
/**
61+
* Construct a new NewCluster component.
62+
*/
63+
constructor(props: NewCluster.IProps) {
64+
super(props);
65+
let model: INewCluster;
66+
model = props.initialModel;
67+
68+
this.state = { model };
69+
}
70+
71+
/**
72+
* When the component updates we take the opportunity to write
73+
* the state of the cluster to an external object so this can
74+
* be sent as the result of the dialog.
75+
*/
76+
componentDidUpdate(): void {
77+
let model: INewCluster = { ...this.state.model };
78+
this.props.stateEscapeHatch(model);
79+
}
80+
81+
/**
82+
* React to the number of workers changing.
83+
*/
84+
onScaleChanged(event: React.ChangeEvent): void {
85+
this.setState({
86+
model: {
87+
...this.state.model,
88+
n: parseInt((event.target as HTMLInputElement).value || null, null),
89+
},
90+
});
91+
}
92+
93+
/**
94+
* React to the number of workers changing.
95+
*/
96+
onProfileChanged(event: React.ChangeEvent): void {
97+
this.setState({
98+
model: {
99+
...this.state.model,
100+
profile: (event.target as HTMLInputElement).value,
101+
},
102+
});
103+
}
104+
105+
/**
106+
* React to the number of workers changing.
107+
*/
108+
onClusterIdChanged(event: React.ChangeEvent): void {
109+
this.setState({
110+
model: {
111+
...this.state.model,
112+
cluster_id: (event.target as HTMLInputElement).value,
113+
},
114+
});
115+
}
116+
117+
/**
118+
* Render the component..
119+
*/
120+
render() {
121+
const model = this.state.model;
122+
// const disabledClass = "ipp-mod-disabled";
123+
return (
124+
<div>
125+
<div className="ipp-DialogSection">
126+
<div className="ipp-DialogSection-item">
127+
<span className={`ipp-DialogSection-label`}>Profile</span>
128+
<input
129+
className="ipp-DialogInput"
130+
value={model.profile}
131+
type="string"
132+
placeholder="default"
133+
onChange={(evt) => {
134+
this.onProfileChanged(evt);
135+
}}
136+
/>
137+
</div>
138+
<div className="ipp-DialogSection-item">
139+
<span className={`ipp-DialogSection-label`}>Cluster ID</span>
140+
<input
141+
className="ipp-DialogInput"
142+
value={model.cluster_id}
143+
type="string"
144+
placeholder="auto"
145+
onChange={(evt) => {
146+
this.onClusterIdChanged(evt);
147+
}}
148+
/>
149+
</div>
150+
<div className="ipp-DialogSection-item">
151+
<span className={`ipp-DialogSection-label`}>Engines</span>
152+
<input
153+
className="ipp-DialogInput"
154+
value={model.n}
155+
type="number"
156+
step="1"
157+
placeholder="auto"
158+
onChange={(evt) => {
159+
this.onScaleChanged(evt);
160+
}}
161+
/>
162+
</div>
163+
</div>
164+
</div>
165+
);
166+
}
167+
}
168+
169+
/**
170+
* Show a dialog for Dialog a cluster model.
171+
*
172+
* @param model: the initial model.
173+
*
174+
* @returns a promse that resolves with the user-selected Dialogs for the
175+
* cluster model. If they pressed the cancel button, it resolves with
176+
* the original model.
177+
*/
178+
export function newClusterDialog(model: INewCluster): Promise<INewCluster> {
179+
let updatedModel = { ...model };
180+
const escapeHatch = (update: INewCluster) => {
181+
updatedModel = update;
182+
};
183+
184+
return showDialog({
185+
title: `New Cluster`,
186+
body: <NewCluster initialModel={model} stateEscapeHatch={escapeHatch} />,
187+
buttons: [Dialog.cancelButton(), Dialog.okButton({ label: "CREATE" })],
188+
}).then((result) => {
189+
if (result.button.accept) {
190+
return updatedModel;
191+
} else {
192+
return null;
193+
}
194+
});
195+
}

0 commit comments

Comments
 (0)