/* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"). You * may not use this file except in compliance with the License. A copy of * the License is located at * * http://aws.amazon.com/apache2.0/ * * or in the "license" file accompanying this file. This file is * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF * ANY KIND, either express or implied. See the License for the specific * language governing permissions and limitations under the License. */ import * as React from 'react'; import { closeIcon, parameterEditorAddItemClass, parameterEditorNoParametersClass, parameterEditorTableClass, } from '../style/ParameterEditor'; import { NotebookPanel } from '@jupyterlab/notebook'; import { IObservableJSON } from '@jupyterlab/observables'; import { JSONObject, JSONArray } from '@lumino/coreutils'; import { ISignal } from '@lumino/signaling'; import { SchedulePanel } from './SchedulePanel'; const METADATA_KEY = 'sagemaker_run_notebook'; export interface ParameterKV { name: string; value: any; // eslint-disable-line @typescript-eslint/no-explicit-any } export interface ParameterEditorProps { onChange(parameterEditor: ParameterEditor): void; notebookPanel: NotebookPanel; notebookPanelChanged: ISignal; } interface ParameterEditorState { parameters: ParameterKV[]; } export type ParameterEntryErrors = Record; export interface ParameterErrors { nameErrors: ParameterEntryErrors; valueErrors: ParameterEntryErrors; } export class ParameterEditor extends React.Component { constructor(props: ParameterEditorProps) { super(props); this.notebookPanelChanged = props.notebookPanelChanged; this.currentNotebookPanel = props.notebookPanel; this.currentMetadata = null; this.onChange = props.onChange; this.state = { parameters: this.extractSavedParameters() }; this.computeNewJSON(false); this.updateParameterTracker(); this.notebookPanelChanged.connect(this.onNotebookPanelChanged, this); } componentWillUnmount() { this.notebookPanelChanged.disconnect(this.onNotebookPanelChanged, this); } private onNotebookPanelChanged(_: SchedulePanel, notebookPanel: NotebookPanel) { this.currentNotebookPanel = notebookPanel; this.getSavedParameters(); this.updateParameterTracker(); } private updateParameterTracker() { if (this.currentMetadata) { this.currentMetadata.changed.disconnect(this.getSavedParameters); } const panel = this.currentNotebookPanel; if (panel) { this.currentMetadata = panel.model.metadata; this.currentMetadata.changed.connect(this.getSavedParameters, this); } } private extractSavedParameters(): ParameterKV[] { let result: ParameterKV[] = []; const panel = this.currentNotebookPanel; if (panel) { const metadata = panel.model.metadata.get(METADATA_KEY) as JSONObject; const params = metadata && ((metadata['saved_parameters'] as unknown) as ParameterKV[]); if (params) { result = params; } } return result; } private getSavedParameters(): void { if (this.currentNotebookPanel) { this.setState({ parameters: this.extractSavedParameters() }, () => this.computeNewJSON(false)); } } private setSavedParameters(): void { const panel = this.currentNotebookPanel; if (panel) { const metadata = (panel.model.metadata.get(METADATA_KEY) as JSONObject) || {}; metadata['saved_parameters'] = (this.state.parameters as unknown) as JSONArray; panel.model.metadata.set(METADATA_KEY, metadata); panel.model.dirty = true; // metadata.set should do this, but doesn't } } render() { let block: JSX.Element; if (!this.state.parameters || this.state.parameters.length === 0) { block = (

No parameters defined. Press “+” to add parameters.

); } else { block = ( {this.state.parameters.map((p, i) => ( ))}
Name Value
) => this.nameChanged(e, i)} value={p.name} title="Parameter" /> ) => this.valueChanged(e, i)} value={p.value} title="Value" /> this.onMinusClick(i)}>
); } return (
Parameters:
{block}
); } private onAddClick = (): void => { const params = this.state.parameters; params.push({ name: '', value: '' }); this.setState({ parameters: params }); }; private onMinusClick = (i: number): void => { const params = this.state.parameters; params.splice(i, 1); this.setState({ parameters: params }); this.computeNewJSON(true); }; private nameChanged = (e: React.ChangeEvent, i: number): void => { const params = this.state.parameters; params[i].name = e.target.value; this.setState({ parameters: params }, () => this.computeNewJSON(true)); }; private valueChanged = (e: React.ChangeEvent, i: number): void => { const params = this.state.parameters; params[i].value = e.target.value; this.setState({ parameters: params }, () => this.computeNewJSON(true)); }; private computeNewJSON(updateMetadata: boolean): void { let valid = true; const seen: Set = new Set(); const result: ParameterKV[] = []; const nameErrors: ParameterEntryErrors = {}; const valueErrors: ParameterEntryErrors = {}; this.state.parameters.forEach((element, idx) => { if (element.name.length === 0) { valid = false; nameErrors[idx] = 'Name must be specified'; } else { if (element.name in seen) { valid = false; nameErrors[idx] = 'Duplicate parameter'; } else { let val = element.value; // this mostly exists to let folks pass numbers as numbers, but in theory they can pass any JSON through. try { val = JSON.parse(val); } catch (SyntaxError) {} // eslint-disable-line no-empty result.push({ name: element.name, value: val }); seen.add(element.name); } } }); this.parametersObject = valid ? result : null; this.nameErrors = nameErrors; this.valueErrors = valueErrors; this.onChange(this); if (updateMetadata) { this.setSavedParameters(); } } get value(): ParameterKV[] { return this.parametersObject; } get errors(): ParameterErrors { return { nameErrors: this.nameErrors, valueErrors: this.valueErrors }; } private onChange: (parameterEditor: ParameterEditor) => void; // The object representation (that will be returned to the caller) private notebookPanelChanged: ISignal; private currentNotebookPanel: NotebookPanel; private currentMetadata: IObservableJSON; private parametersObject: ParameterKV[]; private nameErrors: ParameterEntryErrors; private valueErrors: ParameterEntryErrors; } interface SmallButtonProps { label: string; tooltip: string; onClick(event: React.MouseEvent): void; } function SmallButton(props: SmallButtonProps) { return ( ); }