Skip to content

Commit

Permalink
vdk-jupyter: change Create and Download default path (#2669)
Browse files Browse the repository at this point in the history
What: Updated "Create" and "Download" functionalities to default to the
Jupyter root directory.
And also added tool tip that gives more information about the input
itself. The information only shows up when we hover the question mark
element next to the input label.
![Screenshot 2023-09-18 at 15 12
25](https://github.com/vmware/versatile-data-kit/assets/87015481/73083d56-1c02-44f1-9376-6ba54520f959)




Why: Initial implementations utilised the current working Jupyter
directory. However, usability tests indicated that this approach was
causing confusion. To enhance user experience, the default has been
changed to the root directory.

Signed-off-by: Duygu Hasan [[email protected]](mailto:[email protected])
  • Loading branch information
duyguHsnHsn authored Sep 19, 2023
1 parent 7ef1545 commit b390c3a
Show file tree
Hide file tree
Showing 8 changed files with 184 additions and 119 deletions.

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

0 comments on commit b390c3a

Please sign in to comment.