Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

vdk-jupyter: change Create and Download default path #2669

Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@
"@jupyterlab/ui-components": "3.6.3",
"@lumino/widgets": "1.33.0",
"@types/react": "17.0.53",
"react-icons": "^4.11.0",
"typescript": "4.1.3",
"word-wrap": "1.2.4",
"yjs": "^13.5.17"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,10 @@ describe('#render()', () => {
expect(input).toBe(component.getAllByLabelText('Job Team:')[0]);
});

it('should return contain job path input with placeholder equal to jobPath from props', () => {
it('should return contain job path input with blank placeholder', () => {
const component = render(new CreateJobDialog(defaultProps).render());
const input = component.getByPlaceholderText(defaultProps.jobPath);
expect(input).toBe(
component.getAllByLabelText('Path to job directory:')[0]
);
const jobPathInput = component.getByPlaceholderText(defaultProps.jobPath);
expect(jobPathInput).toBeDefined();
});
});

Expand All @@ -59,7 +57,7 @@ describe('#onTeamChange', () => {
describe('#onPathChange', () => {
it('should change the path in jobData', () => {
const component = render(new CreateJobDialog(defaultProps).render());
const input = component.getByPlaceholderText(defaultProps.jobPath);
const input = component.getAllByRole('textbox')[2];
fireEvent.change(input, { target: { value: 'other/path' } });
expect(jobData.get(VdkOption.PATH)).toEqual('other/path');
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,8 @@ describe('#render()', () => {

it('should return contain job path input with placeholder equal to jobPath from props', () => {
const component = render(new DownloadJobDialog(defaultProps).render());
const input = component.getByPlaceholderText(defaultProps.jobPath);
expect(input).toBe(
component.getAllByLabelText('Path to job directory:')[0]
);
const jobPathInput = component.getByPlaceholderText(defaultProps.jobPath);
expect(jobPathInput).toBeDefined();
});
});

Expand All @@ -65,7 +63,7 @@ describe('#onTeamChange', () => {
describe('#onPathChange', () => {
it('should change the path in jobData', () => {
const component = render(new DownloadJobDialog(defaultProps).render());
const input = component.getByPlaceholderText(defaultProps.jobPath);
const input = component.getAllByRole('textbox')[2];
fireEvent.change(input, { target: { value: 'other/path' } });
expect(jobData.get(VdkOption.PATH)).toEqual('other/path');
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,15 @@ export default class CreateJobDialog extends Component<IJobFullProps> {
option={VdkOption.PATH}
value={this.props.jobPath}
label="Path to job directory:"
></VDKTextInput>
tooltip="Specify the directory for the new job folder, e.g., 'x/y' with job name 'foo' becomes 'x/y/foo'. If left blank, it defaults to the Jupyter's main directory."
></VDKTextInput>
</>
);
}
}

export async function showCreateJobDialog(statusButton: StatusButton) {
jobData.set(VdkOption.PATH, ''); // the default jobPath is the Jupyter root
const result = await showDialog({
title: CREATE_JOB_BUTTON_LABEL,
body: (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,15 @@ export default class DownloadJobDialog extends Component<IJobPathProp> {
option={VdkOption.PATH}
value={this.props.jobPath}
label="Path to job directory:"
tooltip="Specify the directory for the new job folder, e.g., 'x/y' with job name 'foo' becomes 'x/y/foo'. If left blank, it defaults to the Jupyter's main directory."
></VDKTextInput>
</>
);
}
}

export async function showDownloadJobDialog(statusButton?: StatusButton) {
jobData.set(VdkOption.PATH, ''); // the default jobPath is the Jupyter root
const result = await showDialog({
title: DOWNLOAD_JOB_BUTTON_LABEL,
body: (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,31 +1,36 @@
import React, { Component, RefObject } from 'react';
import { jobData } from '../jobData';
import { VdkOption } from '../vdkOptions/vdk_options';
import { FaQuestionCircle } from 'react-icons/fa';

export interface IVdkTextInputProps {
/**
* Represents the VdkOption for which the input is created.
*/
option: VdkOption;
/**
* The default value corresponding to the VdkOption.
*/
value: string;
/**
* The display label for the input.
*/
label: string;
/**
* Callback function that is invoked when the width of the input is computed.
*/
onWidthComputed?: (width: number) => void;
/**
* Represents the VdkOption for which the input is created.
*/
option: VdkOption;
/**
* The default value corresponding to the VdkOption.
*/
value: string;
/**
* The display label for the input.
*/
label: string;
/**
* Callback function that is invoked when the width of the input is computed.
*/
onWidthComputed?: (width: number) => void;
/**
* Optional tooltip content.
*/
tooltip?: string;
}

interface IVdkInputState {
/**
* Represents the computed or default width for the input.
*/
inputWidth: number;
/**
* Represents the computed or default width for the input.
*/
inputWidth: number;
}

/**
Expand All @@ -34,102 +39,117 @@ interface IVdkInputState {
const DEFAULT_INPUT_WIDTH = 250;

export default class VDKTextInput extends Component<IVdkTextInputProps> {
/**
* Component's state.
*/
state: IVdkInputState = {
inputWidth: DEFAULT_INPUT_WIDTH
};
/**
* Component's state.
*/
state: IVdkInputState = {
inputWidth: DEFAULT_INPUT_WIDTH
};

/**
* Reference to the input element.
*/
private inputRef: RefObject<HTMLInputElement> = React.createRef();
/**
* Reference to the input element.
*/
private inputRef: RefObject<HTMLInputElement> = React.createRef();

/**
* Lifecycle method called after the component has mounted. It adjusts the input width based on the content.
*/
componentDidMount(): void {
this.adjustInputWidth();
}
/**
* Lifecycle method called after the component has mounted. It adjusts the input width based on the content.
*/
componentDidMount(): void {
this.adjustInputWidth();
}

/**
* Lifecycle method called after the component updates. It adjusts the input width if the value prop has changed.
*
* @param prevProps - The previous properties before the component updated.
*/
componentDidUpdate(prevProps: IVdkTextInputProps): void {
if (prevProps.value !== this.props.value) {
this.adjustInputWidth();
}
/**
* Lifecycle method called after the component updates. It adjusts the input width if the value prop has changed.
*
* @param prevProps - The previous properties before the component updated.
*/
componentDidUpdate(prevProps: IVdkTextInputProps): void {
if (prevProps.value !== this.props.value) {
this.adjustInputWidth();
}
}

/**
* Adjusts the width of the input field based on the content from jobData.
*
* Utilizes a temporary HTML span element, styled like the input, to determine
* the width required to display each value in jobData without clipping.
* After iterating through all values, the component state's inputWidth is
* updated with the computed maximum width.
*/
adjustInputWidth(): void {
const currentInput = this.inputRef.current;
if (!currentInput) return;
/**
* Adjusts the width of the input field based on the content from jobData.
*
* Utilizes a temporary HTML span element, styled like the input, to determine
* the width required to display each value in jobData without clipping.
* After iterating through all values, the component state's inputWidth is
* updated with the computed maximum width.
*/
adjustInputWidth(): void {
const currentInput = this.inputRef.current;
if (!currentInput) return;

let maxWidth = DEFAULT_INPUT_WIDTH;
let maxWidth = DEFAULT_INPUT_WIDTH;

const tempSpan = document.createElement('span');
const styles = ['fontFamily', 'fontSize', 'fontWeight', 'fontStyle', 'letterSpacing', 'textTransform'];
styles.forEach(style => {
const computedStyle = currentInput ? window.getComputedStyle(currentInput).getPropertyValue(style) : '';
tempSpan.style[style as any] = computedStyle;
});
const tempSpan = document.createElement('span');
const styles = [
'fontFamily',
'fontSize',
'fontWeight',
'fontStyle',
'letterSpacing',
'textTransform'
];
styles.forEach(style => {
const computedStyle = currentInput
? window.getComputedStyle(currentInput).getPropertyValue(style)
: '';
tempSpan.style[style as any] = computedStyle;
});

const PADDING_WIDTH = 100;
jobData.forEach((value) => {
tempSpan.innerHTML = value;
document.body.appendChild(tempSpan);
const spanWidth = tempSpan.getBoundingClientRect().width + PADDING_WIDTH;
document.body.removeChild(tempSpan);
maxWidth = Math.max(maxWidth, spanWidth);
});
const PADDING_WIDTH = 100;
jobData.forEach(value => {
tempSpan.innerHTML = value;
document.body.appendChild(tempSpan);
const spanWidth = tempSpan.getBoundingClientRect().width + PADDING_WIDTH;
document.body.removeChild(tempSpan);
maxWidth = Math.max(maxWidth, spanWidth);
});

this.setState({ inputWidth: maxWidth });
}
this.setState({ inputWidth: maxWidth });
}

/**
* Renders a div containing a label and an input field.
*
* @returns A React element representing the input component.
*/
render(): React.ReactElement {
return (
<div className="jp-vdk-input-wrapper">
<label className="jp-vdk-label" htmlFor={this.props.option}>
{this.props.label}
</label>
<input
ref={this.inputRef}
type="text"
id={this.props.option}
className="jp-vdk-input"
placeholder={this.props.value}
style={{ width: `${this.state.inputWidth}px` }}
onChange={this.onInputChange}
/>
</div>
);
}
/**
* Renders a div containing a label and an input field.
*
* @returns A React element representing the input component.
*/
render(): React.ReactElement {
return (
<div className="jp-vdk-input-wrapper">
<label className="jp-vdk-label" htmlFor={this.props.option}>
{this.props.label}
{this.props.tooltip && (
<span className="tooltip">
<FaQuestionCircle className="icon-tooltip" />
<span className="tooltiptext">{this.props.tooltip}</span>
</span>
)}
</label>
<input
ref={this.inputRef}
type="text"
id={this.props.option}
className="jp-vdk-input"
placeholder={this.props.value}
style={{ width: `${this.state.inputWidth}px` }}
onChange={this.onInputChange}
/>
</div>
);
}

/**
* Callback function invoked when the input value changes. It updates the jobData with the new value.
*
* @param event - The event object containing details about the change event.
*/
private onInputChange = (event: any): void => {
const nameInput = event.currentTarget as HTMLInputElement;
let value = nameInput.value;
if (!value) value = this.props.value;
jobData.set(this.props.option, value);
};
/**
* Callback function invoked when the input value changes. It updates the jobData with the new value.
*
* @param event - The event object containing details about the change event.
*/
private onInputChange = (event: any): void => {
const nameInput = event.currentTarget as HTMLInputElement;
let value = nameInput.value;
if (!value) value = this.props.value;
jobData.set(this.props.option, value);
};
}
Loading