diff --git a/projects/vdk-plugins/vdk-jupyter/vdk-jupyterlab-extension/package-lock.json b/projects/vdk-plugins/vdk-jupyter/vdk-jupyterlab-extension/package-lock.json index 7905bd7d50..fcc633b3f7 100644 --- a/projects/vdk-plugins/vdk-jupyter/vdk-jupyterlab-extension/package-lock.json +++ b/projects/vdk-plugins/vdk-jupyter/vdk-jupyterlab-extension/package-lock.json @@ -36,6 +36,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" @@ -13064,6 +13065,14 @@ "react": "17.0.2" } }, + "node_modules/react-icons": { + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.11.0.tgz", + "integrity": "sha512-V+4khzYcE5EBk/BvcuYRq6V/osf11ODUM2J8hg2FDSswRrGvqiYUYPRy4OdrWaQOBj4NcpJfmHZLNaD+VH0TyA==", + "peerDependencies": { + "react": "*" + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", diff --git a/projects/vdk-plugins/vdk-jupyter/vdk-jupyterlab-extension/package.json b/projects/vdk-plugins/vdk-jupyter/vdk-jupyterlab-extension/package.json index b140eaa55b..8a354915dc 100644 --- a/projects/vdk-plugins/vdk-jupyter/vdk-jupyterlab-extension/package.json +++ b/projects/vdk-plugins/vdk-jupyter/vdk-jupyterlab-extension/package.json @@ -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" diff --git a/projects/vdk-plugins/vdk-jupyter/vdk-jupyterlab-extension/src/__tests__/create-job-component.spec.ts b/projects/vdk-plugins/vdk-jupyter/vdk-jupyterlab-extension/src/__tests__/create-job-component.spec.ts index dd8d52d898..aa30ebe741 100644 --- a/projects/vdk-plugins/vdk-jupyter/vdk-jupyterlab-extension/src/__tests__/create-job-component.spec.ts +++ b/projects/vdk-plugins/vdk-jupyter/vdk-jupyterlab-extension/src/__tests__/create-job-component.spec.ts @@ -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(); }); }); @@ -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'); }); diff --git a/projects/vdk-plugins/vdk-jupyter/vdk-jupyterlab-extension/src/__tests__/download-job-component.spec.ts b/projects/vdk-plugins/vdk-jupyter/vdk-jupyterlab-extension/src/__tests__/download-job-component.spec.ts index 752fb365d7..fe118e8c17 100644 --- a/projects/vdk-plugins/vdk-jupyter/vdk-jupyterlab-extension/src/__tests__/download-job-component.spec.ts +++ b/projects/vdk-plugins/vdk-jupyter/vdk-jupyterlab-extension/src/__tests__/download-job-component.spec.ts @@ -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(); }); }); @@ -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'); }); diff --git a/projects/vdk-plugins/vdk-jupyter/vdk-jupyterlab-extension/src/components/CreateJob.tsx b/projects/vdk-plugins/vdk-jupyter/vdk-jupyterlab-extension/src/components/CreateJob.tsx index 32c956ddab..0a0be08f56 100644 --- a/projects/vdk-plugins/vdk-jupyter/vdk-jupyterlab-extension/src/components/CreateJob.tsx +++ b/projects/vdk-plugins/vdk-jupyter/vdk-jupyterlab-extension/src/components/CreateJob.tsx @@ -40,13 +40,15 @@ export default class CreateJobDialog extends Component { 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." + > ); } } 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: ( diff --git a/projects/vdk-plugins/vdk-jupyter/vdk-jupyterlab-extension/src/components/DownloadJob.tsx b/projects/vdk-plugins/vdk-jupyter/vdk-jupyterlab-extension/src/components/DownloadJob.tsx index a81c8d1d9e..857d07a84a 100644 --- a/projects/vdk-plugins/vdk-jupyter/vdk-jupyterlab-extension/src/components/DownloadJob.tsx +++ b/projects/vdk-plugins/vdk-jupyter/vdk-jupyterlab-extension/src/components/DownloadJob.tsx @@ -40,6 +40,7 @@ export default class DownloadJobDialog extends Component { 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." > ); @@ -47,6 +48,7 @@ export default class DownloadJobDialog extends Component { } 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: ( diff --git a/projects/vdk-plugins/vdk-jupyter/vdk-jupyterlab-extension/src/components/VdkTextInput.tsx b/projects/vdk-plugins/vdk-jupyter/vdk-jupyterlab-extension/src/components/VdkTextInput.tsx index 9798486a95..9a76e94a17 100644 --- a/projects/vdk-plugins/vdk-jupyter/vdk-jupyterlab-extension/src/components/VdkTextInput.tsx +++ b/projects/vdk-plugins/vdk-jupyter/vdk-jupyterlab-extension/src/components/VdkTextInput.tsx @@ -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; } /** @@ -34,102 +39,117 @@ interface IVdkInputState { const DEFAULT_INPUT_WIDTH = 250; export default class VDKTextInput extends Component { - /** - * 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 = React.createRef(); + /** + * Reference to the input element. + */ + private inputRef: RefObject = 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 ( -
- - -
- ); - } + /** + * Renders a div containing a label and an input field. + * + * @returns A React element representing the input component. + */ + render(): React.ReactElement { + return ( +
+ + +
+ ); + } - /** - * 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); + }; } diff --git a/projects/vdk-plugins/vdk-jupyter/vdk-jupyterlab-extension/style/vdkDialogs.css b/projects/vdk-plugins/vdk-jupyter/vdk-jupyterlab-extension/style/vdkDialogs.css index c16fe53ff0..d76a417d04 100644 --- a/projects/vdk-plugins/vdk-jupyter/vdk-jupyterlab-extension/style/vdkDialogs.css +++ b/projects/vdk-plugins/vdk-jupyter/vdk-jupyterlab-extension/style/vdkDialogs.css @@ -7,6 +7,7 @@ display: flex; flex-direction: column; margin-top: 15px; + position: relative; } .jp-vdk-input { @@ -149,3 +150,37 @@ padding: 20px; width: 600px; } + +.tooltip { + position: relative; + display: inline-block; + cursor: help; + margin-left: 5px; +} + +.icon-tooltip { + cursor: pointer; + color: #888; +} + +.tooltiptext { + visibility: hidden; + min-width: 200px; + background-color: rgb(118 118 118); + color: #fff; + text-align: center; + border-radius: 6px; + padding: 5px; + position: absolute; + z-index: 0; + bottom: 125%; + left: 50%; + margin-left: -120px; + opacity: 0; + transition: opacity 0.3s; +} + +.tooltip:hover .tooltiptext { + visibility: visible; + opacity: 1; +}