diff --git a/src/odo.ts b/src/odo.ts index 3c7aa8154..787f7e53f 100644 --- a/src/odo.ts +++ b/src/odo.ts @@ -100,19 +100,21 @@ export interface Odo { * * @param componentPath the folder in which to create the project * @param componentName the name of the component + * @param portNumber the port number used in the component * @param devfileName the name of the devfile to use * @param registryName the name of the devfile registry that the devfile comes from * @param templateProjectName the template project from the devfile to use */ - createComponentFromTemplateProject(componentPath: string, componentName: string, devfileName: string, registryName: string, templateProjectName: string): Promise; + createComponentFromTemplateProject(componentPath: string, componentName: string, portNumber: number, devfileName: string, registryName: string, templateProjectName: string): Promise; /** * Create a component from the given local codebase. * * @param devfileName the name of the devfile to use * @param componentName the name of the component + * @param portNumber the port number used in the component * @param location the location of the local codebase */ - createComponentFromLocation(devfileName: string, componentName: string, location: Uri): Promise; + createComponentFromLocation(devfileName: string, componentName: string, portNumber: number, location: Uri): Promise; } export class OdoImpl implements Odo { @@ -242,7 +244,7 @@ export class OdoImpl implements Odo { } public async createComponentFromFolder(type: string, registryName: string, name: string, location: Uri, starter: string = undefined, useExistingDevfile = false, customDevfilePath = ''): Promise { - await this.execute(Command.createLocalComponent(type, registryName, name, starter, useExistingDevfile, customDevfilePath), location.fsPath); + await this.execute(Command.createLocalComponent(type, registryName, name, undefined, starter, useExistingDevfile, customDevfilePath), location.fsPath); let wsFolder: WorkspaceFolder; if (workspace.workspaceFolders) { // could be new or existing folder @@ -253,12 +255,12 @@ export class OdoImpl implements Odo { } } - public async createComponentFromLocation(devfileName: string, componentName: string, location: Uri): Promise { - await this.execute(Command.createLocalComponent(devfileName, undefined, componentName, undefined, false, ''), location.fsPath); + public async createComponentFromLocation(devfileName: string, componentName: string, portNumber: number, location: Uri): Promise { + await this.execute(Command.createLocalComponent(devfileName, undefined, componentName, portNumber, undefined, false, ''), location.fsPath); } - public async createComponentFromTemplateProject(componentPath: string, componentName: string, devfileName: string, registryName: string, templateProjectName: string): Promise { - await this.execute(Command.createLocalComponent(devfileName, registryName, componentName, templateProjectName), componentPath); + public async createComponentFromTemplateProject(componentPath: string, componentName: string, portNumber: number, devfileName: string, registryName: string, templateProjectName: string): Promise { + await this.execute(Command.createLocalComponent(devfileName, registryName, componentName, portNumber, templateProjectName), componentPath); } public async createService(formData: any): Promise { diff --git a/src/odo/command.ts b/src/odo/command.ts index 21cf359a4..f80fabaa5 100644 --- a/src/odo/command.ts +++ b/src/odo/command.ts @@ -206,6 +206,7 @@ export class Command { type = '', // will use empty string in case of undefined type passed in registryName: string, name: string, + portNumber: number, starter: string = undefined, useExistingDevfile = false, customDevfilePath = '', @@ -233,6 +234,9 @@ export class Command { if (devfileVersion) { cTxt.addOption(new CommandOption('--devfile-version', devfileVersion, false)); } + if (portNumber) { + cTxt.addOption(new CommandOption(' --run-port', portNumber.toString(), false)); + } return cTxt; } diff --git a/src/odo/componentType.ts b/src/odo/componentType.ts index 2eb22ba15..6ad020a4c 100644 --- a/src/odo/componentType.ts +++ b/src/odo/componentType.ts @@ -88,6 +88,7 @@ export interface AnalyzeResponse { devfile: string; devfileRegistry: string; devfileVersion: string; + ports: number[]; } export type ComponentTypeDescription = DevfileComponentType & DevfileData; diff --git a/src/openshift/nameValidator.ts b/src/openshift/nameValidator.ts index 93717ea46..36ce090b0 100644 --- a/src/openshift/nameValidator.ts +++ b/src/openshift/nameValidator.ts @@ -10,8 +10,8 @@ export function emptyName(message: string, value: string): string | null { return validator.isEmpty(value) ? message : null; } -export function lengthName(message: string, value: string, offset: number): string | null { - return validator.isLength(value, {min: 2, max: 63 - offset}) ? null : message; +export function lengthName(message: string, value: string, offset: number, minVal = 2, maxVal = 63): string | null { + return validator.isLength(value, { min: minVal, max: maxVal - offset }) ? null : message; } export function validateUrl(message: string, value: string): string | null { diff --git a/src/webview/common-ext/createComponentHelpers.ts b/src/webview/common-ext/createComponentHelpers.ts index d5aee46c7..56a29d41b 100644 --- a/src/webview/common-ext/createComponentHelpers.ts +++ b/src/webview/common-ext/createComponentHelpers.ts @@ -109,6 +109,33 @@ export function validateComponentName(name: string): string { return validationMessage; } +/** + * Returns the validation message if the component name is invalid, and undefined otherwise. + * + * @param name the port number to validate + * @returns the validation message if the component name is invalid, and undefined otherwise + */ +export function validatePortNumber(portNumber: number): string { + let validationMessage: string | null; + const port = portNumber.toString(); + if (NameValidator.emptyName('Empty', port) === null) { + validationMessage = NameValidator.lengthName( + 'Port number length should be between 1-5 digits', + port, + 0, + 1, + 5 + ); + + if (!validationMessage) { + if (portNumber < 1 || portNumber > 65535) { + validationMessage = 'Not a valid port number.' + } + } + } + return validationMessage; +} + /** * Returns a list of the devfile registries with their devfiles attached. * @@ -130,7 +157,7 @@ export function getDevfileRegistries(): DevfileRegistry[] { const components = ComponentTypesView.instance.getCompDescriptions(); for (const component of components) { const devfileRegistry = devfileRegistries.find( - (devfileRegistry) => format(devfileRegistry.url) === format(component.registry.url), + (devfileRegistry) => format(devfileRegistry.url) === format(component.registry.url), ); devfileRegistry.devfiles.push({ diff --git a/src/webview/common/componentNameInput.tsx b/src/webview/common/componentNameInput.tsx index 7209a61e4..f34463636 100644 --- a/src/webview/common/componentNameInput.tsx +++ b/src/webview/common/componentNameInput.tsx @@ -27,7 +27,6 @@ export function ComponentNameInput(props: ComponentNameInputProps) { data: e.target.value }); props.setComponentName(e.target.value); - }} - /> + }} /> ); } diff --git a/src/webview/common/createComponentButton.tsx b/src/webview/common/createComponentButton.tsx index 62e0468d1..dee7c7a7f 100644 --- a/src/webview/common/createComponentButton.tsx +++ b/src/webview/common/createComponentButton.tsx @@ -11,10 +11,12 @@ export type CreateComponentButtonProps = { componentName: string; componentParentFolder: string; addToWorkspace: boolean; + portNumber: number; isComponentNameFieldValid: boolean; + isPortNumberFieldValid: boolean; isFolderFieldValid: boolean; isLoading: boolean; - createComponent: (projectFolder: string, componentName: string, isAddToWorkspace: boolean) => void; + createComponent: (projectFolder: string, componentName: string, isAddToWorkspace: boolean, portNumber: number) => void; setLoading: React.Dispatch>; }; @@ -23,10 +25,10 @@ export function CreateComponentButton(props: CreateComponentButtonProps) { { - props.createComponent(props.componentParentFolder, props.componentName, props.addToWorkspace); + props.createComponent(props.componentParentFolder, props.componentName, props.addToWorkspace, props.portNumber); props.setLoading(true); }} - disabled={!props.isComponentNameFieldValid || !props.isFolderFieldValid || props.isLoading} + disabled={!props.isComponentNameFieldValid || !props.isPortNumberFieldValid || !props.isFolderFieldValid || props.isLoading} loading={props.isLoading} loadingPosition="start" startIcon={} diff --git a/src/webview/common/devfile.ts b/src/webview/common/devfile.ts index 0d380aed1..d5a2c56fb 100644 --- a/src/webview/common/devfile.ts +++ b/src/webview/common/devfile.ts @@ -8,6 +8,7 @@ import { StarterProject } from '../../odo/componentTypeDescription'; export type Devfile = { name: string; id: string; + port: number; registryName: string; description: string; logoUrl: string; diff --git a/src/webview/common/fromTemplateProject.tsx b/src/webview/common/fromTemplateProject.tsx index 94c90c27c..7a2eb410e 100644 --- a/src/webview/common/fromTemplateProject.tsx +++ b/src/webview/common/fromTemplateProject.tsx @@ -26,13 +26,14 @@ export function FromTemplateProject(props: FromTemplateProjectProps) { setCurrentPage((_) => 'setNameAndFolder'); } - function createComponent(projectFolder: string, componentName: string, addToWorkspace: boolean) { + function createComponent(projectFolder: string, componentName: string, addToWorkspace: boolean, portNumber: number) { window.vscodeApi.postMessage({ action: 'createComponent', data: { templateProject: selectedTemplateProject, projectFolder, componentName, + portNumber, isFromTemplateProject: true, addToWorkspace }, diff --git a/src/webview/common/portNumberInput.tsx b/src/webview/common/portNumberInput.tsx new file mode 100644 index 000000000..e65090df1 --- /dev/null +++ b/src/webview/common/portNumberInput.tsx @@ -0,0 +1,33 @@ +/*----------------------------------------------------------------------------------------------- + * Copyright (c) Red Hat, Inc. All rights reserved. + * Licensed under the MIT License. See LICENSE file in the project root for license information. + *-----------------------------------------------------------------------------------------------*/ +import { TextField } from '@mui/material'; +import * as React from 'react'; + +export type PortNumberInputProps = { + portNumber: number, + isPortNumberFieldValid: boolean, + portNumberErrorMessage: string, + setPortNumber: React.Dispatch> +}; + +export function PortNumberInput(props: PortNumberInputProps) { + return ( + { + window.vscodeApi.postMessage({ + action: 'validatePortNumber', + data: e.target.value + }); + props.setPortNumber(parseInt(e.target.value, 10)); + }} /> + ); +} diff --git a/src/webview/common/setNameAndFolder.tsx b/src/webview/common/setNameAndFolder.tsx index 4b97e30a6..fb1c0c2dc 100644 --- a/src/webview/common/setNameAndFolder.tsx +++ b/src/webview/common/setNameAndFolder.tsx @@ -19,6 +19,7 @@ import { ComponentNameInput } from './componentNameInput'; import { CreateComponentButton, CreateComponentErrorAlert } from './createComponentButton'; import { Devfile } from './devfile'; import { DevfileListItem } from './devfileListItem'; +import { PortNumberInput } from './portNumberInput'; type Message = { action: string; @@ -27,7 +28,7 @@ type Message = { type SetNameAndFolderProps = { goBack: () => void; - createComponent: (projectFolder: string, componentName: string, addToWorkspace: boolean) => void; + createComponent: (projectFolder: string, componentName: string, addToWorkspace: boolean, portNumber: number) => void; devfile: Devfile; templateProject?: string; initialComponentName?: string; @@ -35,11 +36,15 @@ type SetNameAndFolderProps = { export function SetNameAndFolder(props: SetNameAndFolderProps) { const [componentName, setComponentName] = React.useState(props.initialComponentName); + const [portNumber, setPortNumber] = React.useState(props.devfile.port); const [isComponentNameFieldValid, setComponentNameFieldValid] = React.useState(true); const [componentNameErrorMessage, setComponentNameErrorMessage] = React.useState( 'Please enter a component name.', ); - + const [isPortNumberFieldValid, setPortNumberFieldValid] = React.useState(true); + const [portNumberErrorMessage, setPortNumberErrorMessage] = React.useState( + 'Port number auto filled based on devfile selection', + ); const [componentParentFolder, setComponentParentFolder] = React.useState(''); const [isFolderFieldValid, setFolderFieldValid] = React.useState(false); const [folderFieldErrorMessage, setFolderFieldErrorMessage] = React.useState(''); @@ -73,6 +78,16 @@ export function SetNameAndFolder(props: SetNameAndFolderProps) { } break; } + case 'validatePortNumber': { + if (message.data) { + setPortNumberFieldValid(false); + setPortNumberErrorMessage(message.data); + } else { + setPortNumberFieldValid(true); + setPortNumberErrorMessage(''); + } + break; + } case 'createComponentFailed': { setLoading(false); setCreateComponentErrorMessage(message.data); @@ -109,6 +124,15 @@ export function SetNameAndFolder(props: SetNameAndFolderProps) { } }, []); + React.useEffect(() => { + if (props.devfile.port) { + window.vscodeApi.postMessage({ + action: 'validatePortNumber', + data: `${props.devfile.port}`, + }); + } + }, []); + return (
@@ -138,6 +162,15 @@ export function SetNameAndFolder(props: SetNameAndFolderProps) { componentName={componentName} setComponentName={setComponentName} /> + { + portNumber && + + } devfile.name === compDescriptions[0].displayName, ) : undefined; + if (devfile) { + devfile.port = compDescriptions[0].devfileData.devfile.components[0].container?.endpoints[0].targetPort; + } void CreateComponentLoader.panel.webview.postMessage({ action: 'recommendedDevfile', data: { @@ -567,4 +585,4 @@ function sendUpdatedTags() { data: getDevfileTags(), }); } -} \ No newline at end of file +} diff --git a/src/webview/create-component/pages/fromExistingGitRepo.tsx b/src/webview/create-component/pages/fromExistingGitRepo.tsx index 0078c0f98..5f6bf47a6 100644 --- a/src/webview/create-component/pages/fromExistingGitRepo.tsx +++ b/src/webview/create-component/pages/fromExistingGitRepo.tsx @@ -132,6 +132,7 @@ export function FromExistingGitRepo({ setCurrentView }) { projectFolder: string, componentName: string, addToWorkspace: boolean, + portNumber: number ) { window.vscodeApi.postMessage({ action: 'createComponent', @@ -142,6 +143,7 @@ export function FromExistingGitRepo({ setCurrentView }) { componentName, gitDestinationPath: projectFolder, isFromTemplateProject: false, + portNumber, addToWorkspace, }, }); diff --git a/src/webview/create-component/pages/fromLocalCodebase.tsx b/src/webview/create-component/pages/fromLocalCodebase.tsx index f0fd6ee25..3bc9f5990 100644 --- a/src/webview/create-component/pages/fromLocalCodebase.tsx +++ b/src/webview/create-component/pages/fromLocalCodebase.tsx @@ -25,6 +25,7 @@ import { DevfileListItem } from '../../common/devfileListItem'; import { DevfileRecommendationInfo } from '../../common/devfileRecommendationInfo'; import { DevfileSearch } from '../../common/devfileSearch'; import { NoSuitableDevfile } from '../../common/noSuitableDevfile'; +import { PortNumberInput } from '../../common/portNumberInput'; type Message = { action: string; @@ -33,12 +34,6 @@ type Message = { type CurrentPage = 'fromLocalCodeBase' | 'selectDifferentDevfile'; -export type ComponentNameState = { - name: string; - isValid: boolean; - helpText: string; -}; - type RecommendedDevfileState = { devfile: Devfile; showRecommendation: boolean; @@ -56,10 +51,15 @@ export function FromLocalCodebase(props: FromLocalCodebaseProps) { const [workspaceFolders, setWorkspaceFolders] = React.useState([]); const [projectFolder, setProjectFolder] = React.useState(''); const [componentName, setComponentName] = React.useState(''); + const [portNumber, setPortNumber] = React.useState(undefined); const [isComponentNameFieldValid, setComponentNameFieldValid] = React.useState(true); const [componentNameErrorMessage, setComponentNameErrorMessage] = React.useState( 'Please enter a component name.', ); + const [isPortNumberFieldValid, setPortNumberFieldValid] = React.useState(true); + const [portNumberErrorMessage, setPortNumberErrorMessage] = React.useState( + 'Port number auto filled based on devfile selection', + ); const [isLoading, setLoading] = React.useState(false); const [isLoaded, setLoaded] = React.useState(false); @@ -92,6 +92,9 @@ export function FromLocalCodebase(props: FromLocalCodebaseProps) { showRecommendation: Boolean(message.data.devfile), })); setRecommendedDevfile((prevState) => ({ ...prevState, isLoading: false })); + setPortNumber(message.data.devfile.port); + setPortNumberFieldValid(true); + setPortNumberErrorMessage(''); break; } case 'validatedComponentName': { @@ -104,6 +107,16 @@ export function FromLocalCodebase(props: FromLocalCodebaseProps) { } break; } + case 'validatePortNumber': { + if (message.data) { + setPortNumberFieldValid(false); + setPortNumberErrorMessage(message.data); + } else { + setPortNumberFieldValid(true); + setPortNumberErrorMessage(''); + } + break; + } case 'selectedProjectFolder': { setProjectFolder(message.data); break; @@ -161,7 +174,7 @@ export function FromLocalCodebase(props: FromLocalCodebaseProps) { setRecommendedDevfile((prevState) => ({ ...prevState, isLoading: true })); } - function createComponentFromLocalCodebase(projectFolder: string, componentName: string, addToWorkspace: boolean) { + function createComponentFromLocalCodebase(projectFolder: string, componentName: string, addToWorkspace: boolean, portNumber: number) { window.vscodeApi.postMessage({ action: 'createComponent', data: { @@ -169,6 +182,7 @@ export function FromLocalCodebase(props: FromLocalCodebaseProps) { ? selectedDevfile.name : recommendedDevfile.devfile.name, componentName, + portNumber, path: projectFolder, isFromTemplateProject: false, addToWorkspace, @@ -179,219 +193,229 @@ export function FromLocalCodebase(props: FromLocalCodebaseProps) { switch (currentPage) { case 'fromLocalCodeBase': - return ( - <> -
- From Existing Local Codebase -
- - - - - Folder - - {recommendedDevfile.isDevfileExistsInFolder && ( - - A devfile already exists in this project, please select - another folder. - - )} - {workspaceFolders.length === 0 && ( - - There are no projects in the workspace, select folder or - open a folder in the workspace. - - )} - - {!recommendedDevfile.showRecommendation && ( - - )} - - {!isLoaded ? ( - <> - - {(!props.rootFolder || props.rootFolder.length === 0) && - - } - - - {recommendedDevfile.isLoading && ( - - - - - Scanning for recommended devfile. - - )} - - ) : recommendedDevfile.showRecommendation || selectedDevfile ? ( - <> - - - <> - - - {selectedDevfile - ? 'Selected Devfile' - : 'Recommended Devfile'} - - {!selectedDevfile && } - - - - - + + {!isLoaded ? ( + <> + + {(!props.rootFolder || props.rootFolder.length === 0) && + + } - {(recommendedDevfile.showRecommendation || - selectedDevfile) && ( - - )} - - - - ) : ( - <> - - - - - - + + {(recommendedDevfile.showRecommendation || + selectedDevfile) && ( + + )} + + + + + ) : ( + <> + + + + - SELECT A DEVFILE - + + + - - - )} -
- - ); + + )} + + + ); + } case 'selectDifferentDevfile': return ( <> diff --git a/src/webview/devfile-registry/registryViewLoader.ts b/src/webview/devfile-registry/registryViewLoader.ts index a933f082b..f4d4d34ec 100644 --- a/src/webview/devfile-registry/registryViewLoader.ts +++ b/src/webview/devfile-registry/registryViewLoader.ts @@ -12,7 +12,7 @@ import sendTelemetry from '../../telemetry'; import { ExtensionID } from '../../util/constants'; import { selectWorkspaceFolder } from '../../util/workspace'; import { vsCommand } from '../../vscommand'; -import { getDevfileCapabilities, getDevfileRegistries, getDevfileTags, isValidProjectFolder, validateComponentName } from '../common-ext/createComponentHelpers'; +import { getDevfileCapabilities, getDevfileRegistries, getDevfileTags, isValidProjectFolder, validateComponentName, validatePortNumber } from '../common-ext/createComponentHelpers'; import { loadWebviewHtml } from '../common-ext/utils'; import { TemplateProjectIdentifier } from '../common/devfile'; @@ -47,12 +47,14 @@ async function devfileRegistryViewerMessageListener(event: any): Promise { case 'createComponent': { const { projectFolder, componentName } = event.data; const templateProject: TemplateProjectIdentifier = event.data.templateProject; + const portNumber: number = event.data.portNumber; const componentFolder = path.join(projectFolder, componentName); try { await fs.mkdir(componentFolder); await OdoImpl.Instance.createComponentFromTemplateProject( componentFolder, componentName, + portNumber, templateProject.devfileId, templateProject.registryName, templateProject.templateProjectName, @@ -86,6 +88,17 @@ async function devfileRegistryViewerMessageListener(event: any): Promise { }); break; } + /** + * The panel requested to validate the entered port number. Respond with error status and message. + */ + case 'validatePortNumber': { + const validationMessage = validatePortNumber(event.data); + void panel.webview.postMessage({ + action: 'validatePortNumber', + data: validationMessage, + }); + break; + } case 'selectProjectFolderNewProject': { const workspaceUri: vscode.Uri = await selectWorkspaceFolder(true); void panel.webview.postMessage({ @@ -213,4 +226,4 @@ ComponentTypesView.instance.subject.subscribe(() => { ComponentTypesView.instance.subject.subscribe(() => { RegistryViewLoader.sendUpdatedTags(); -}); \ No newline at end of file +}); diff --git a/test/integration/command.test.ts b/test/integration/command.test.ts index dba626d09..2a8c5e237 100644 --- a/test/integration/command.test.ts +++ b/test/integration/command.test.ts @@ -332,6 +332,7 @@ suite('odo commands integration', function () { componentType, 'DefaultDevfileRegistry', componentName, + 8080, componentStarterProject, undefined, undefined, @@ -677,6 +678,7 @@ suite('odo commands integration', function () { componentType, 'DefaultDevfileRegistry', componentName, + undefined, componentStarterProject, undefined, undefined,