From 0c95ed10a20e2a4c7236c51bd8bdb5508534ec78 Mon Sep 17 00:00:00 2001 From: Victor Rubezhny Date: Tue, 9 Jul 2024 19:50:26 +0200 Subject: [PATCH] Devfile Registry editor is to allow devfile version selection and use when creating a component #4189 Fixes: #4189 Issue: #3850 Signed-off-by: Victor Rubezhny --- src/cli.ts | 4 + src/devfile-registry/devfileInfo.ts | 65 ++ .../devfileRegistryWrapper.ts | 183 +++++ src/odo/command.ts | 16 +- src/odo/components.ts | 2 + src/odo/odoWrapper.ts | 174 +++-- src/openshift/component.ts | 317 ++++---- src/registriesView.ts | 164 ++-- .../common-ext/createComponentHelpers.ts | 188 +++-- src/webview/common/createComponentButton.tsx | 5 +- src/webview/common/devfile.ts | 1 + src/webview/common/devfileListItem.tsx | 74 +- src/webview/common/devfileSearch.tsx | 734 +++++++++++------- src/webview/common/fromTemplateProject.tsx | 33 +- src/webview/common/setNameAndFolder.tsx | 37 +- .../create-component/createComponentLoader.ts | 216 +++--- .../pages/fromExistingGitRepo.tsx | 21 +- .../pages/fromLocalCodebase.tsx | 47 +- .../createDeploymentLoader.ts | 4 +- src/webview/create-route/app/createForm.tsx | 26 +- src/webview/create-service/app/createForm.tsx | 20 +- .../devfile-registry/registryViewLoader.ts | 67 +- src/webview/helm-chart/app/helmSearch.tsx | 12 +- .../app/invokeFunction.tsx | 6 +- src/webview/tsconfig.json | 8 +- test/integration/alizerWrapper.test.ts | 12 +- test/integration/command.test.ts | 8 +- .../devfileRegistryWrapper.test.ts | 58 ++ test/integration/odoWrapper.test.ts | 75 +- test/unit/openshift/component.test.ts | 42 +- 30 files changed, 1646 insertions(+), 973 deletions(-) create mode 100644 src/devfile-registry/devfileInfo.ts create mode 100644 src/devfile-registry/devfileRegistryWrapper.ts create mode 100644 test/integration/devfileRegistryWrapper.test.ts diff --git a/src/cli.ts b/src/cli.ts index b28f391e4..3117c8f24 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -5,6 +5,7 @@ import { VSCodeSettings } from '@redhat-developer/vscode-redhat-telemetry/lib/common/vscode/settings'; import * as cp from 'child_process'; +import * as hasha from 'hasha'; import * as vscode from 'vscode'; import { CommandText } from './base/command'; import { ToolsConfig } from './tools'; @@ -12,6 +13,9 @@ import { ChildProcessUtil, CliExitData } from './util/childProcessUtil'; import { VsCommandError } from './vscommand'; export class ExecutionContext extends Map { + public static key(value: string): string { + return hasha(value); + } } export class CliChannel { diff --git a/src/devfile-registry/devfileInfo.ts b/src/devfile-registry/devfileInfo.ts new file mode 100644 index 000000000..5c6bd3d9d --- /dev/null +++ b/src/devfile-registry/devfileInfo.ts @@ -0,0 +1,65 @@ +/*----------------------------------------------------------------------------------------------- + * Copyright (c) Red Hat, Inc. All rights reserved. + * Licensed under the MIT License. See LICENSE file in the project root for license information. + *-----------------------------------------------------------------------------------------------*/ +// +// The Types representing the output of +// 'odo registry [--devfile-registry="" -o=json' +// + +import { Data } from '../odo/componentTypeDescription'; + +export type DevfileRegistryInfo = { + name: string, + url: string, + secure: boolean + }; + +export type DevfileLinksInfo = { + any +}; + +export type DevfileCommandGroupsInfo = { + build: boolean, + debug: boolean, + deploy: boolean, + run: boolean, + test: boolean +}; + +export type DevfileVersionInfo = { + version: string, + schemaVersion: string, + default: boolean, + description: string, + tags: string[], + icon: string, + links: DevfileLinksInfo, + commandGroups: DevfileCommandGroupsInfo, + resources: string[], + starterProjects: string[], +}; + +export type DevfileInfo = { + name: string, + displayName: string, + description: string, + type: string, + tags: string[], + architectures: string[], + icon: string, + projectType: string, + language: string, + provider: string, + supportUrl: string, + versions: DevfileVersionInfo[], + registry?: DevfileRegistryInfo +}; + +export type DevfileInfoExt = DevfileInfo & { + proposedVersion?: string +}; + +export type DevfileData = Data & { + yaml: string; +}; \ No newline at end of file diff --git a/src/devfile-registry/devfileRegistryWrapper.ts b/src/devfile-registry/devfileRegistryWrapper.ts new file mode 100644 index 000000000..a74af5ec3 --- /dev/null +++ b/src/devfile-registry/devfileRegistryWrapper.ts @@ -0,0 +1,183 @@ +/*----------------------------------------------------------------------------------------------- + * Copyright (c) Red Hat, Inc. All rights reserved. + * Licensed under the MIT License. See LICENSE file in the project root for license information. + *-----------------------------------------------------------------------------------------------*/ +import * as https from 'https'; +import * as YAML from 'js-yaml'; +import { ExecutionContext } from '../cli'; +import { Registry } from '../odo/componentType'; +import { Odo } from '../odo/odoWrapper'; +import { DevfileData, DevfileInfo } from './devfileInfo'; + +export const DEVFILE_VERSION_LATEST: string = 'latest'; + +/** + * Wraps some the Devfile Registry REST API calls. + */ +export class DevfileRegistry { + private static instance: DevfileRegistry; + + private executionContext: ExecutionContext = new ExecutionContext(); + + public static get Instance(): DevfileRegistry { + if (!DevfileRegistry.instance) { + DevfileRegistry.instance = new DevfileRegistry(); + } + return DevfileRegistry.instance; + } + + private constructor() { + // no state + } + + /** + * Get list of Devfile Infos from Registry. + * + * GET http://{registry host}/v2index/all + * + * @param url Devfile Registry URL + * @param abortTimeout (Optional) If provided, allow cancelling the operation by timeout + * @param abortController (Optional) If provided, allows cancelling the operation by signal + */ + public async getDevfileInfoList(url: string, abortTimeout?: number, abortController?: AbortController): Promise { + const requestUrl = `${url}/v2index/all`; + const key = ExecutionContext.key(requestUrl); + if (this.executionContext && this.executionContext.has(key)) { + return this.executionContext.get(key); + } + const rawList = await DevfileRegistry._get(`${url}/v2index/all`, abortTimeout, abortController); + const jsonList = JSON.parse(rawList); + this.executionContext.set(key, jsonList); + return jsonList; + } + + /** + * Get Devfile of specified version from Registry. + * + * GET http://{registry host}/devfiles/{stack}/{version} + * + * @param url Devfile Registry URL + * @param stack Devfile stack + * @param version (Optional) If specified, the version of Devfile to be received, otherwize 'latest' version is requested + * @param abortTimeout (Optional) If provided, allow cancelling the operation by timeout + * @param abortController (Optional) If provided, allows cancelling the operation by signal + */ + public async _getDevfile(url: string, stack: string, version?: string, abortTimeout?: number, abortController?: AbortController): Promise { + const requestUrl = `${url}/devfiles/${stack}/${version ? version : DEVFILE_VERSION_LATEST}`; + const key = ExecutionContext.key(requestUrl); + if (this.executionContext && this.executionContext.has(key)) { + return this.executionContext.get(key); + } + const devfile = DevfileRegistry._get(`${url}/devfiles/${stack}/${version ? version : DEVFILE_VERSION_LATEST}`, + abortTimeout, abortController); + this.executionContext.set(key, devfile); + return devfile; + } + + /** + * Returns a list of the devfile registries from ODO preferences. + * + * @returns a list of the devfile registries + */ + public async getRegistries(registryUrl?: string): Promise { + // Return only registries registered for user (from ODO preferences) + // and filter by registryUrl (if provided) + + let registries: Registry[] = []; + const key = ExecutionContext.key('getRegistries'); + if (this.executionContext && !this.executionContext.has(key)) { + registries = await Odo.Instance.getRegistries(); + this.executionContext.set(key, registries); + } else { + registries = this.executionContext.get(key); + } + + return !registries ? [] : + registries.filter((reg) => { + if (registryUrl) { + return (reg.url === registryUrl) + } + return true; + }); + } + + /** + * Returns a list of the devfile registries with their devfiles attached. + * + * @returns a list of the devfile registries with their devfiles attached + */ + public async getRegistryDevfileInfos(registryUrl?: string): Promise { + // Return only registries registered for user (from ODO preferences) + // and filter by registryUrl (if provided) + const registries: Registry[] = await this.getRegistries(registryUrl); + if (!registries || registries.length === 0) { + throw new Error('No Devfile registries available. Default registry is missing'); + } + + const devfiles: DevfileInfo[] = []; + await Promise.all(registries + .map(async (registry): Promise => { + const devfileInfoList = (await this.getDevfileInfoList(registry.url)) + .filter((devfileInfo) => 'stack' === devfileInfo.type.toLowerCase()); + devfileInfoList.forEach((devfileInfo) => { + devfileInfo.registry = registry; + }); + devfiles.push(...devfileInfoList); + })); + + return devfiles.sort((a, b) => (a.name < b.name ? -1 : 1)); + } + + /** + * Returns a devfile data with raw devvile text attached + * + * @returns a devfile data with raw devvile text attached + */ + public async getRegistryDevfile(registryUrl: string, name: string, version?: string): Promise { + const rawDevfile = await this._getDevfile(registryUrl, name, version ? version : 'latest'); + const devfile = YAML.load(rawDevfile) as DevfileData; + devfile.yaml = rawDevfile; + return devfile; + } + + private static async _get(url: string, abortTimeout?: number, abortController?: AbortController): Promise { + return new Promise((resolve, reject) => { + const signal = abortController?.signal; + const timeout = abortTimeout ? abortTimeout : 5000; + const options = { rejectUnauthorized: false, signal, timeout }; + let result: string = ''; + https.get(url, options, (response) => { + if (response.statusCode < 500) { + response.on('data', (d) => { + result = result.concat(d); + }); + response.resume(); + response.on('end', () => { + if (!response.complete) { + reject(new Error(`The connection was terminated while the message was still being sent: ${response.statusMessage}`)); + } else { + resolve(result); + } + }); + } else { + reject(new Error(`Connect error: ${response.statusMessage}`)); + } + }).on('error', (e) => { + reject(new Error(`Connect error: ${e}`)); + }).on('success', (s) => { + resolve(result); + }); + }); + } + + /** + * Clear the Execution context as well as all cached data + */ + public clearCache() { + if (this.executionContext) { + this.executionContext.clear(); + } + this.executionContext = new ExecutionContext(); + } + +} \ No newline at end of file diff --git a/src/odo/command.ts b/src/odo/command.ts index 9b7b29e74..71ed9e209 100644 --- a/src/odo/command.ts +++ b/src/odo/command.ts @@ -62,21 +62,24 @@ export class Command { @verbose static createLocalComponent( - type = '', // will use empty string in case of undefined type passed in + devfileType = '', // will use empty string in case of undefined devfileType passed in + devfileVersion: string = undefined, registryName: string, name: string, portNumber: number, starter: string = undefined, useExistingDevfile = false, - customDevfilePath = '', - devfileVersion: string = undefined, + customDevfilePath = '' ): CommandText { const cTxt = new CommandText('odo', 'init', [ new CommandOption('--name', name) ] ); - if (type !== '') { - cTxt.addOption(new CommandOption('--devfile', type)); + if (devfileType !== '') { + cTxt.addOption(new CommandOption('--devfile', devfileType)); + } + if (devfileVersion) { + cTxt.addOption(new CommandOption('--devfile-version', devfileVersion, false)); } if (registryName) { cTxt.addOption(new CommandOption('--devfile-registry', registryName)); @@ -90,9 +93,6 @@ export class Command { if (customDevfilePath.length > 0) { cTxt.addOption(new CommandOption('--devfile-path', customDevfilePath, false)); } - if (devfileVersion) { - cTxt.addOption(new CommandOption('--devfile-version', devfileVersion, false)); - } if (portNumber) { cTxt.addOption(new CommandOption(' --run-port', portNumber.toString(), false)); } diff --git a/src/odo/components.ts b/src/odo/components.ts index 437f6f768..9f3fa8d4e 100644 --- a/src/odo/components.ts +++ b/src/odo/components.ts @@ -3,6 +3,8 @@ * Licensed under the MIT License. See LICENSE file in the project root for license information. *-----------------------------------------------------------------------------------------------*/ +// TODO: Not used. A candidate to removal? + export interface ComponentsJson { // list is not present when there are no components // i.e. if there are no components the JSON is `{}` diff --git a/src/odo/odoWrapper.ts b/src/odo/odoWrapper.ts index 52cd39380..7912d6020 100644 --- a/src/odo/odoWrapper.ts +++ b/src/odo/odoWrapper.ts @@ -11,8 +11,8 @@ import { ToolsConfig } from '../tools'; import { ChildProcessUtil, CliExitData } from '../util/childProcessUtil'; import { VsCommandError } from '../vscommand'; import { Command } from './command'; -import { ComponentTypeAdapter, ComponentTypeDescription, DevfileComponentType, Registry } from './componentType'; -import { ComponentDescription, StarterProject } from './componentTypeDescription'; +import { Registry } from './componentType'; +import { ComponentDescription } from './componentTypeDescription'; import { BindableService } from './odoTypes'; /** @@ -32,31 +32,41 @@ export class Odo { // no state } - public async getComponentTypes(): Promise { - // if kc is produced, KUBECONFIG env var is empty or pointing + // // TODO: NOT USED. A CANDIDATE TO REMOVAL? + // public async getComponentTypes(): Promise { + // // if kc is produced, KUBECONFIG env var is empty or pointing - const result: CliExitData = await this.execute( - new CommandText('odo', 'registry -o json'), - undefined, - true - ); - const componentTypes: DevfileComponentType[] = this.loadJSON(result.stdout); - const devfileItems: ComponentTypeAdapter[] = []; + // const result: CliExitData = await this.execute( + // new CommandText('odo', 'registry -o json'), + // undefined, + // true + // ); + // const componentTypes: DevfileComponentType[] = this.loadJSON(result.stdout); + // const devfileItems: ComponentTypeAdapter[] = []; - componentTypes.map((item) => - devfileItems.push( - new ComponentTypeAdapter( - item.name, - undefined, - item.description, - undefined, - item.registry.name, - ), - ), - ); + // componentTypes.map((item) => + // devfileItems.push( + // new ComponentTypeAdapter( + // item.name, + // undefined, + // item.description, + // undefined, + // item.registry.name, + // ), + // ), + // ); - return devfileItems; - } + // return devfileItems; + // } + + // public async getRegistryDevfileInfos(registryName?: string): Promise { + // const command: CommandText = new CommandText('odo', 'registry -o json'); + // if (registryName) { + // command.addOption(new CommandOption('--devfile-registry', registryName, true)); + // } + // const result: CliExitData = await this.execute(command, undefined, true); + // return this.loadJSON(result.stdout); + // } public async describeComponent( contextPath: string, @@ -103,22 +113,24 @@ export class Odo { public async createComponentFromFolder( type: string, + version: string = undefined, registryName: string, name: string, location: Uri, starter: string = undefined, useExistingDevfile = false, - customDevfilePath = '', + customDevfilePath = '' ): Promise { await this.execute( Command.createLocalComponent( type, + version, registryName, name, undefined, starter, useExistingDevfile, - customDevfilePath, + customDevfilePath ), location.fsPath, ); @@ -140,12 +152,14 @@ export class Odo { * Create a component from the given local codebase. * * @param devfileName the name of the devfile to use + * @param devfileVersion the version of the devfile to use * @param componentName the name of the component * @param portNumber the port to expose on the container that runs the code * @param location the location of the local codebase */ public async createComponentFromLocation( devfileName: string, + devfileVersion: string, componentName: string, portNumber: number, location: Uri, @@ -153,12 +167,13 @@ export class Odo { await this.execute( Command.createLocalComponent( devfileName, + devfileVersion, undefined, componentName, portNumber, undefined, false, - '', + '' ), location.fsPath, ); @@ -171,6 +186,7 @@ export class Odo { * @param componentName the name of the component * @param portNumber the port to expose on the container that runs the code * @param devfileName the name of the devfile to use + * @param devfileVersion the version 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 */ @@ -179,12 +195,14 @@ export class Odo { componentName: string, portNumber: number, devfileName: string, + devfileVersion: string, registryName: string, templateProjectName: string, ): Promise { await this.execute( Command.createLocalComponent( devfileName, + devfileVersion, registryName, componentName, portNumber, @@ -194,49 +212,52 @@ export class Odo { ); } - /** - * Returns a list of starter projects for the given Devfile - * - * TODO: write integration test - * - * @param componentType the Devfile information - * @returns the list of starter projects - */ - public async getStarterProjects( - componentType: ComponentTypeAdapter, - ): Promise { - const descr = await Odo.Instance.execute(this.describeCatalogComponent(componentType)); - try { - const rawJson = JSON.parse(descr.stdout); - const dfCompType = rawJson.find( - (comp) => comp.registry.name === componentType.registryName, - ); - if (dfCompType.devfileData.devfile.starterProjects) { - return dfCompType.devfileData.devfile.starterProjects as StarterProject[]; - } - } catch { - // ignore parse errors and return empty array - } - return []; - } + // // TODO: NOT USED. A CANDIDATE TO REMOVAL? + // /** + // * Returns a list of starter projects for the given Devfile + // * + // * TODO: write integration test + // * + // * @param componentType the Devfile information + // * @returns the list of starter projects + // */ + // public async getStarterProjects( + // componentType: ComponentTypeAdapter, + // ): Promise { + // const descr = await Odo.Instance.execute(this.describeCatalogComponent(componentType)); + // try { + // const rawJson = JSON.parse(descr.stdout); + // const dfCompType = rawJson.find( + // (comp) => comp.registry.name === componentType.registryName, + // ); + // if (dfCompType.devfileData.devfile.starterProjects) { + // return dfCompType.devfileData.devfile.starterProjects as StarterProject[]; + // } + // } catch { + // // ignore parse errors and return empty array + // } + // return []; + // } - public async getDetailedComponentInformation( - componentType: ComponentTypeAdapter, - ): Promise { - const result = await this.execute(this.describeCatalogComponent(componentType)); - const [componentTypeDetails] = JSON.parse(result.stdout) as ComponentTypeDescription[]; - return componentTypeDetails; - } + // TODO: NOT USED. A CANDIDATE TO REMOVAL? + // public async getDetailedComponentInformation( + // componentType: ComponentTypeAdapter, + // ): Promise { + // const result = await this.execute(this.describeCatalogComponent(componentType)); + // const [componentTypeDetails] = JSON.parse(result.stdout) as ComponentTypeDescription[]; + // return componentTypeDetails; + // } - private loadJSON(json: string): I { - let data: I; - try { - data = JSON.parse(json); - } catch { - // ignore parse errors and return empty array - } - return data; - } + // // TODO: NOT USED. A CANDIDATE TO REMOVAL? + // private loadJSON(json: string): I { + // let data: I; + // try { + // data = JSON.parse(json); + // } catch { + // // ignore parse errors and return empty array + // } + // return data; + // } private async loadRegistryFromPreferences() { const cliData = await this.execute(new CommandText('odo', 'preference view -o json')); @@ -342,12 +363,13 @@ export class Odo { }); } - private describeCatalogComponent(componentType: ComponentTypeAdapter): CommandText { - return new CommandText('odo', 'registry', [ - new CommandOption('--details'), - new CommandOption('--devfile', componentType.name), - new CommandOption('--devfile-registry', componentType.registryName), - new CommandOption('-o', 'json', false), - ]); - } + // // TODO: NOT USED. A CANDIDATE TO REMOVAL? + // private describeCatalogComponent(componentType: ComponentTypeAdapter): CommandText { + // return new CommandText('odo', 'registry', [ + // new CommandOption('--details'), + // new CommandOption('--devfile', componentType.name), + // new CommandOption('--devfile-registry', componentType.registryName), + // new CommandOption('-o', 'json', false), + // ]); + // } } diff --git a/src/openshift/component.ts b/src/openshift/component.ts index 7f3fc646c..f75333ed6 100644 --- a/src/openshift/component.ts +++ b/src/openshift/component.ts @@ -4,18 +4,16 @@ *-----------------------------------------------------------------------------------------------*/ import * as fs from 'fs/promises'; -import * as JSYAML from 'js-yaml'; import { platform } from 'os'; import * as path from 'path'; import { which } from 'shelljs'; import { commands, debug, DebugConfiguration, DebugSession, Disposable, EventEmitter, extensions, ProgressLocation, Uri, window, workspace } from 'vscode'; import { Oc } from '../oc/ocWrapper'; import { Command } from '../odo/command'; -import { ascDevfileFirst, ComponentTypeAdapter } from '../odo/componentType'; -import { CommandProvider, StarterProject } from '../odo/componentTypeDescription'; +import { CommandProvider } from '../odo/componentTypeDescription'; import { Odo } from '../odo/odoWrapper'; import { ComponentWorkspaceFolder } from '../odo/workspace'; -import sendTelemetry, { NewComponentCommandProps } from '../telemetry'; +import sendTelemetry from '../telemetry'; import { ChildProcessUtil, CliExitData } from '../util/childProcessUtil'; import { Progress } from '../util/progress'; import { vsCommand, VsCommandError } from '../vscommand'; @@ -24,13 +22,14 @@ import CreateComponentLoader from '../webview/create-component/createComponentLo import { OpenShiftTerminalApi, OpenShiftTerminalManager } from '../webview/openshift-terminal/openShiftTerminal'; import OpenShiftItem, { clusterRequired, projectRequired } from './openshiftItem'; -function createCancelledResult(stepName: string): any { - const cancelledResult: any = new String(''); - cancelledResult.properties = { - 'cancelled_step': stepName - } - return cancelledResult; -} +// // TODO: NOT USED. A CANDIDATE TO REMOVAL? +// function createCancelledResult(stepName: string): any { +// const cancelledResult: any = new String(''); +// cancelledResult.properties = { +// 'cancelled_step': stepName +// } +// return cancelledResult; +// } function createStartDebuggerResult(language: string, message = '') { const result: any = new String(message); @@ -482,153 +481,155 @@ export class Component extends OpenShiftItem { await CreateComponentLoader.loadView('Create Component', context.fsPath); } - /** - * Create a component - * - * @param folder The folder to use as component context folder - * @param selection The folders selected in case of multiple selection in Explorer view. - * @param context - * @param componentTypeName - * @param componentKind - * @returns A thenable that resolves to the message to show or empty string if components is already exists or null if command is canceled. - * @throws VsCommandError or Error in case of error in cli or code - */ - - static async createFromRootWorkspaceFolder(folder: Uri, selection: Uri[], opts: { - componentTypeName?: string, - projectName?: string, - applicationName?: string, - compName?: string, - registryName?: string - devFilePath?: string - }, isGitImportCall = false): Promise { - let useExistingDevfile = false; - const devFileLocation = path.join(folder.fsPath, 'devfile.yaml'); - try { - await fs.access(devFileLocation); - useExistingDevfile = true; - } catch { - // do not use existing devfile - } - - let initialNameValue: string; - if (useExistingDevfile) { - const file = await fs.readFile(devFileLocation, 'utf8'); - const devfileYaml = JSYAML.load(file.toString()) as any; - - if (devfileYaml && devfileYaml.metadata && devfileYaml.metadata.name) { - initialNameValue = devfileYaml.metadata.name; - } - } - - const progressIndicator = window.createQuickPick(); - - let createStarter: string; - let componentType: ComponentTypeAdapter; - let componentTypeCandidates: ComponentTypeAdapter[]; - if (!useExistingDevfile && (!opts || !opts.devFilePath || opts.devFilePath.length === 0)) { - const componentTypes = await Odo.Instance.getComponentTypes(); - progressIndicator.busy = true; - progressIndicator.placeholder = opts?.componentTypeName ? `Checking if '${opts.componentTypeName}' Component type is available` : 'Loading available Component types'; - progressIndicator.show(); - if (opts?.componentTypeName) { - componentTypeCandidates = opts.registryName && opts.registryName.length > 0 ? componentTypes.filter(type => type.name === opts.componentTypeName && type.registryName === opts.registryName) : componentTypes.filter(type => type.name === opts.componentTypeName); - if (componentTypeCandidates?.length === 0) { - componentType = await window.showQuickPick(componentTypes.sort(ascDevfileFirst), { placeHolder: `Cannot find Component type '${opts.componentTypeName}', select one below to use instead`, ignoreFocusOut: true }); - } else if (componentTypeCandidates?.length > 1) { - componentType = await window.showQuickPick(componentTypeCandidates.sort(ascDevfileFirst), { placeHolder: `Found more than one Component types '${opts.componentTypeName}', select one below to use`, ignoreFocusOut: true }); - } else { - [componentType] = componentTypeCandidates; - progressIndicator.hide(); - } - } else { - componentType = await window.showQuickPick(componentTypes.sort(ascDevfileFirst), { placeHolder: 'Select Component type', ignoreFocusOut: true }); - } - - if (!componentType) return createCancelledResult('componentType'); - - progressIndicator.placeholder = 'Checking if provided context folder is empty' - progressIndicator.show(); - const workspacePath = `${folder.fsPath.replaceAll('\\', '/')}/`; - const dirIsEmpty = (await fs.readdir(workspacePath)).length === 0; - progressIndicator.hide(); - if (dirIsEmpty && !isGitImportCall) { - if (opts?.projectName) { - createStarter = opts.projectName; - } else { - progressIndicator.placeholder = 'Loading Starter Projects for selected Component Type' - progressIndicator.show(); - - const starterProjects: StarterProject[] = await Odo.Instance.getStarterProjects(componentType); - progressIndicator.hide(); - if (starterProjects?.length && starterProjects.length > 0) { - const create = await window.showQuickPick(['Yes', 'No'], { placeHolder: `Initialize Component using ${starterProjects.length === 1 ? '\''.concat(starterProjects[0].name.concat('\' ')) : ''}Starter Project?` }); - if (create === 'Yes') { - if (starterProjects.length === 1) { - createStarter = starterProjects[0].name; - } else { - const selectedStarter = await window.showQuickPick( - starterProjects.map(prj => ({ label: prj.name, description: prj.description })), - { placeHolder: 'Select Starter Project to initialize Component' } - ); - if (!selectedStarter) return createCancelledResult('selectStarterProject'); - createStarter = selectedStarter.label; - } - } else if (!create) { - return createCancelledResult('useStaterProjectRequest');; - } - } - } - } - } - - const componentName = opts?.compName || await Component.getName( - 'Name', - initialNameValue?.trim().length > 0 ? initialNameValue : createStarter - ); - - if (!componentName) return createCancelledResult('componentName'); - const refreshComponentsView = workspace.getWorkspaceFolder(folder); - const createComponentProperties: NewComponentCommandProps = { - 'component_kind': 'devfile', - 'component_type': componentType?.name, - 'component_version': componentType?.version, - 'starter_project': createStarter, - 'use_existing_devfile': useExistingDevfile, - }; - try { - await Progress.execFunctionWithProgress( - `Creating new Component '${componentName}'`, - () => Odo.Instance.createComponentFromFolder( - componentType?.name, // in case of using existing devfile - componentType?.registryName, - componentName, - folder, - createStarter, - useExistingDevfile, - opts?.devFilePath - ) - ); - - // when creating component based on existing workspace folder refresh components view - if (refreshComponentsView) { - void commands.executeCommand('openshift.componentsView.refresh'); - } - - const result: any = new String(`Component '${componentName}' successfully created. Perform actions on it from Components View.`); - result.properties = createComponentProperties; - return result; - } catch (err) { - if (err instanceof VsCommandError) { - throw new VsCommandError( - `Error occurred while creating Component '${componentName}': ${err.message}`, - `Error occurred while creating Component: ${err.telemetryMessage}`, err, - createComponentProperties - ); - } - throw err; - } - } + // // TODO: NOT USED. A CANDIDATE TO REMOVAL? + // /** + // * Create a component + // * + // * @param folder The folder to use as component context folder + // * @param selection The folders selected in case of multiple selection in Explorer view. + // * @param context + // * @param componentTypeName + // * @param componentKind + // * @returns A thenable that resolves to the message to show or empty string if components is already exists or null if command is canceled. + // * @throws VsCommandError or Error in case of error in cli or code + // */ + + // static async createFromRootWorkspaceFolder(folder: Uri, selection: Uri[], opts: { + // componentTypeName?: string, + // projectName?: string, + // applicationName?: string, + // compName?: string, + // registryName?: string + // devFilePath?: string + // }, isGitImportCall = false): Promise { + // let useExistingDevfile = false; + // const devFileLocation = path.join(folder.fsPath, 'devfile.yaml'); + // try { + // await fs.access(devFileLocation); + // useExistingDevfile = true; + // } catch { + // // do not use existing devfile + // } + + // let initialNameValue: string; + // if (useExistingDevfile) { + // const file = await fs.readFile(devFileLocation, 'utf8'); + // const devfileYaml = JSYAML.load(file.toString()) as any; + + // if (devfileYaml && devfileYaml.metadata && devfileYaml.metadata.name) { + // initialNameValue = devfileYaml.metadata.name; + // } + // } + + // const progressIndicator = window.createQuickPick(); + + // let createStarter: string; + // let componentType: ComponentTypeAdapter; + // let componentTypeCandidates: ComponentTypeAdapter[]; + // if (!useExistingDevfile && (!opts || !opts.devFilePath || opts.devFilePath.length === 0)) { + // const componentTypes = await Odo.Instance.getComponentTypes(); + // progressIndicator.busy = true; + // progressIndicator.placeholder = opts?.componentTypeName ? `Checking if '${opts.componentTypeName}' Component type is available` : 'Loading available Component types'; + // progressIndicator.show(); + // if (opts?.componentTypeName) { + // componentTypeCandidates = opts.registryName && opts.registryName.length > 0 ? componentTypes.filter(type => type.name === opts.componentTypeName && type.registryName === opts.registryName) : componentTypes.filter(type => type.name === opts.componentTypeName); + // if (componentTypeCandidates?.length === 0) { + // componentType = await window.showQuickPick(componentTypes.sort(ascDevfileFirst), { placeHolder: `Cannot find Component type '${opts.componentTypeName}', select one below to use instead`, ignoreFocusOut: true }); + // } else if (componentTypeCandidates?.length > 1) { + // componentType = await window.showQuickPick(componentTypeCandidates.sort(ascDevfileFirst), { placeHolder: `Found more than one Component types '${opts.componentTypeName}', select one below to use`, ignoreFocusOut: true }); + // } else { + // [componentType] = componentTypeCandidates; + // progressIndicator.hide(); + // } + // } else { + // componentType = await window.showQuickPick(componentTypes.sort(ascDevfileFirst), { placeHolder: 'Select Component type', ignoreFocusOut: true }); + // } + + // if (!componentType) return createCancelledResult('componentType'); + + // progressIndicator.placeholder = 'Checking if provided context folder is empty' + // progressIndicator.show(); + // const workspacePath = `${folder.fsPath.replaceAll('\\', '/')}/`; + // const dirIsEmpty = (await fs.readdir(workspacePath)).length === 0; + // progressIndicator.hide(); + // if (dirIsEmpty && !isGitImportCall) { + // if (opts?.projectName) { + // createStarter = opts.projectName; + // } else { + // progressIndicator.placeholder = 'Loading Starter Projects for selected Component Type' + // progressIndicator.show(); + + // const starterProjects: StarterProject[] = await Odo.Instance.getStarterProjects(componentType); + // progressIndicator.hide(); + // if (starterProjects?.length && starterProjects.length > 0) { + // const create = await window.showQuickPick(['Yes', 'No'], { placeHolder: `Initialize Component using ${starterProjects.length === 1 ? '\''.concat(starterProjects[0].name.concat('\' ')) : ''}Starter Project?` }); + // if (create === 'Yes') { + // if (starterProjects.length === 1) { + // createStarter = starterProjects[0].name; + // } else { + // const selectedStarter = await window.showQuickPick( + // starterProjects.map(prj => ({ label: prj.name, description: prj.description })), + // { placeHolder: 'Select Starter Project to initialize Component' } + // ); + // if (!selectedStarter) return createCancelledResult('selectStarterProject'); + // createStarter = selectedStarter.label; + // } + // } else if (!create) { + // return createCancelledResult('useStaterProjectRequest');; + // } + // } + // } + // } + // } + + // const componentName = opts?.compName || await Component.getName( + // 'Name', + // initialNameValue?.trim().length > 0 ? initialNameValue : createStarter + // ); + + // if (!componentName) return createCancelledResult('componentName'); + // const refreshComponentsView = workspace.getWorkspaceFolder(folder); + // const createComponentProperties: NewComponentCommandProps = { + // 'component_kind': 'devfile', + // 'component_type': componentType?.name, + // 'component_version': componentType?.version, + // 'starter_project': createStarter, + // 'use_existing_devfile': useExistingDevfile, + // }; + // try { + // await Progress.execFunctionWithProgress( + // `Creating new Component '${componentName}'`, + // () => Odo.Instance.createComponentFromFolder( + // componentType?.name, // in case of using existing devfile + // componentType?.version, + // componentType?.registryName, + // componentName, + // folder, + // createStarter, + // useExistingDevfile, + // opts?.devFilePath + // ) + // ); + + // // when creating component based on existing workspace folder refresh components view + // if (refreshComponentsView) { + // void commands.executeCommand('openshift.componentsView.refresh'); + // } + + // const result: any = new String(`Component '${componentName}' successfully created. Perform actions on it from Components View.`); + // result.properties = createComponentProperties; + // return result; + // } catch (err) { + // if (err instanceof VsCommandError) { + // throw new VsCommandError( + // `Error occurred while creating Component '${componentName}': ${err.message}`, + // `Error occurred while creating Component: ${err.telemetryMessage}`, err, + // createComponentProperties + // ); + // } + // throw err; + // } + // } @vsCommand('openshift.component.debug', true) static async debug(component: ComponentWorkspaceFolder): Promise { diff --git a/src/registriesView.ts b/src/registriesView.ts index 945214fa6..ce7f69bad 100644 --- a/src/registriesView.ts +++ b/src/registriesView.ts @@ -2,7 +2,7 @@ * Copyright (c) Red Hat, Inc. All rights reserved. * Licensed under the MIT License. See LICENSE file in the project root for license information. *-----------------------------------------------------------------------------------------------*/ -import { Subject } from 'rxjs'; +// import { Subject } from 'rxjs'; import validator from 'validator'; import * as vscode from 'vscode'; import { @@ -10,9 +10,8 @@ import { QuickInputButtons, QuickPickItem, ThemeIcon, TreeDataProvider, TreeItem, TreeItemCollapsibleState, TreeView, Uri, window } from 'vscode'; +import { DevfileRegistry } from './devfile-registry/devfileRegistryWrapper'; import { - ComponentTypeDescription, - DevfileComponentType, Registry } from './odo/componentType'; import { StarterProject } from './odo/componentTypeDescription'; @@ -43,14 +42,14 @@ export class ComponentTypesView implements TreeDataProvider { readonly odo = Odo.Instance; private registries: Registry[]; - private readonly compDescriptions: Set = new Set(); - public subject: Subject = new Subject(); + // private readonly compDescriptions: Set = new Set(); + // public subject: Subject = new Subject(); - private initialComponentTypeLoadPromise: Promise; + // private initialComponentTypeLoadPromise: Promise; constructor() { - this.initialComponentTypeLoadPromise = this.reloadComponentTypeList(); - void Progress.execFunctionWithProgress('Loading component types', () => this.initialComponentTypeLoadPromise); + // this.initialComponentTypeLoadPromise = this.reloadComponentTypeList(); + // void Progress.execFunctionWithProgress('Loading component types', () => this.initialComponentTypeLoadPromise); } createTreeView(id: string): TreeView { @@ -80,26 +79,60 @@ export class ComponentTypesView implements TreeDataProvider { }; } + // eslint-disable-next-line class-methods-use-this + async getChildren(parent: ComponentType): Promise { + let children: ComponentType[] = []; + if (!parent) { + const result = await this.getRegistries(); + const newChildren = result.filter((reg) => reg.name === 'DefaultDevfileRegistry') + .concat(result.filter((reg) => reg.name !== 'DefaultDevfileRegistry').sort()); + children = newChildren; + } + return children; + } + + getParent?(): ComponentType { + return undefined; + } + addRegistry(newRegistry: Registry): void { if (!this.registries) { this.registries = []; } this.registries.push(newRegistry); + this.refresh(false); this.reveal(newRegistry); } removeRegistry(targetRegistry: Registry): void { + if (!this.registries) { + this.registries = []; + } this.registries.splice( this.registries.findIndex((registry) => registry.name === targetRegistry.name), 1, ); - this.refresh(false); + this.refresh(true); + } + + replaceRegistry(targetRegistry: Registry, newRegistry: Registry): void { + if (!this.registries) { + this.registries = []; + } + this.registries.splice( + this.registries.findIndex((registry) => registry.name === targetRegistry.name), + 1, + ); + this.registries.push(newRegistry); + this.refresh(true); + this.reveal(newRegistry); } - public async getRegistries(): Promise { + + private async getRegistries(): Promise { try { if (!this.registries) { - this.registries = await this.odo.getRegistries(); + this.registries = await DevfileRegistry.Instance.getRegistries(); } } catch { this.registries = []; @@ -107,54 +140,37 @@ export class ComponentTypesView implements TreeDataProvider { return this.registries; } - public async getCompDescriptions(): Promise> { - await this.initialComponentTypeLoadPromise; - return this.compDescriptions; - } + // public async getCompDescriptions(): Promise> { + // await this.initialComponentTypeLoadPromise; + // return this.compDescriptions; + // } public getListOfRegistries(): Registry[] { return this.registries; } - private async reloadComponentTypeList(): Promise { - this.compDescriptions.clear(); - try { - const devfileComponentTypes = await Odo.Instance.getComponentTypes(); - await this.getRegistries(); - await Promise.all(devfileComponentTypes.map(async (devfileComponentType) => { - const componentDesc: ComponentTypeDescription = await Odo.Instance.getDetailedComponentInformation(devfileComponentType); - componentDesc.devfileData.devfile?.starterProjects?.map((starter: StarterProject) => { - starter.typeName = devfileComponentType.name; - }); - this.compDescriptions.add(componentDesc); - - if (devfileComponentTypes.length === this.compDescriptions.size) { - this.subject.next(); - } - })); - this.subject.next(); - } catch { - this.subject.next(); - } - } + // private async reloadComponentTypeList(): Promise { + // this.compDescriptions.clear(); + // try { + // const devfileComponentTypes = await Odo.Instance.getComponentTypes(); + // await this.getRegistries(); + // await Promise.all(devfileComponentTypes.map(async (devfileComponentType) => { + // const componentDesc: ComponentTypeDescription = await Odo.Instance.getDetailedComponentInformation(devfileComponentType); + // componentDesc.devfileData.devfile?.starterProjects?.map((starter: StarterProject) => { + // starter.typeName = devfileComponentType.name; + // }); + // this.compDescriptions.add(componentDesc); + + // if (devfileComponentTypes.length === this.compDescriptions.size) { + // this.subject.next(); + // } + // })); + // this.subject.next(); + // } catch { + // this.subject.next(); + // } + // } - // eslint-disable-next-line class-methods-use-this - async getChildren(parent: ComponentType): Promise { - let children: ComponentType[] = []; - if (!parent) { - this.registries = await this.getRegistries(); - /** - * no need to show the default devfile registry on tree view - */ - children = this.registries; - } - return children; - } - - // eslint-disable-next-line class-methods-use-this - getParent?(): ComponentType { - return undefined; - } reveal(item: Registry): void { void this.treeView.reveal(item); @@ -163,6 +179,7 @@ export class ComponentTypesView implements TreeDataProvider { refresh(cleanCache = true): void { if (cleanCache) { this.registries = undefined; + DevfileRegistry.Instance.clearCache(); } this.onDidChangeTreeDataEmitter.fire(undefined); } @@ -335,28 +352,22 @@ export class ComponentTypesView implements TreeDataProvider { break; } case Step.createOrChangeRegistry: { - /** - * For edit, remove the existing registry - */ - if (registryContext) { - const notChangedRegisty = registries?.find((registry) => registry.name === regName && registry.url === regURL && registry.secure === (secure === 'Yes')); - if (notChangedRegisty) { - return null; - } - await vscode.commands.executeCommand('openshift.componentTypesView.registry.remove', registryContext, true); - } - try { - const response = await fetch(regURL, { method: 'GET' }); - const componentTypes = JSON.parse(await response.text()) as DevfileComponentType[]; - if (componentTypes.length > 0) { - void Progress.execFunctionWithProgress('Devfile registry is updating',async () => { + void Progress.execFunctionWithProgress('Devfile registry is updating',async () => { + if (registryContext) { + const notChangedRegisty = registries?.find((registry) => registry.name === regName && registry.url === regURL && registry.secure === (secure === 'Yes')); + if (notChangedRegisty) { + return null; + } + } + + const devfileInfos = await DevfileRegistry.Instance.getDevfileInfoList(regURL); + if (devfileInfos.length > 0) { + await Odo.Instance.removeRegistry(registryContext.name); const newRegistry = await Odo.Instance.addRegistry(regName, regURL, token); - ComponentTypesView.instance.addRegistry(newRegistry); - await ComponentTypesView.instance.reloadComponentTypeList(); - ComponentTypesView.instance.refresh(false); - }) - } + ComponentTypesView.instance.replaceRegistry(registryContext, newRegistry); + } + }); } catch { void vscode.window.showErrorMessage(`Invalid registry URL ${regURL}`); } @@ -379,9 +390,10 @@ export class ComponentTypesView implements TreeDataProvider { if (yesNo === 'Yes') { await Odo.Instance.removeRegistry(registry.name); ComponentTypesView.instance.removeRegistry(registry); - if (!isEdit) { - await ComponentTypesView.instance.reloadComponentTypeList(); - } + // ComponentTypesView.instance.refresh(); + // if (!isEdit) { + // await ComponentTypesView.instance.reloadComponentTypeList(); + // } } } diff --git a/src/webview/common-ext/createComponentHelpers.ts b/src/webview/common-ext/createComponentHelpers.ts index 22b795449..5290a8271 100644 --- a/src/webview/common-ext/createComponentHelpers.ts +++ b/src/webview/common-ext/createComponentHelpers.ts @@ -3,13 +3,11 @@ * Licensed under the MIT License. See LICENSE file in the project root for license information. *-----------------------------------------------------------------------------------------------*/ import * as fs from 'fs/promises'; -import * as JSYAML from 'js-yaml'; import * as path from 'path'; -import { format } from 'url'; -import { Registry } from '../../odo/componentType'; +import { WebviewPanel } from 'vscode'; +import { DevfileInfo } from '../../devfile-registry/devfileInfo'; +import { DevfileRegistry } from '../../devfile-registry/devfileRegistryWrapper'; import * as NameValidator from '../../openshift/nameValidator'; -import { ComponentTypesView } from '../../registriesView'; -import { Devfile, DevfileRegistry } from '../common/devfile'; import { ValidationResult, ValidationStatus } from '../common/validationResult'; /** @@ -180,68 +178,73 @@ export function validatePortNumber(portNumber: number): string { return validationMessage; } -/** - * Returns a list of the devfile registries with their devfiles attached. - * - * @returns a list of the devfile registries with their devfiles attached - */ -export async function getDevfileRegistries(): Promise { - const registries = ComponentTypesView.instance.getListOfRegistries(); - if (!registries || registries.length === 0) { - throw new Error('No Devfile registries available. Default registry is missing'); - } - const devfileRegistries = registries.map((registry: Registry) => { - return { - devfiles: [], - name: registry.name, - url: registry.url, - } as DevfileRegistry; - }); +// /** +// * Returns a list of the devfile registries with their devfiles attached. +// * +// * @returns a list of the devfile registries with their devfiles attached +// */ +// export function getDevfileRegistries(registryUrl?: string): Promise { +// const registries = ComponentTypesView.instance.getListOfRegistries(); +// if (!registries || registries.length === 0) { +// throw new Error('No Devfile registries available. Default registry is missing'); +// } +// const devfileRegistries = registries.map((registry: Registry) => { +// return { +// devfiles: [], +// name: registry.name, +// url: registry.url, +// } as DevfileRegistry; +// }); - const components = await ComponentTypesView.instance.getCompDescriptions(); - for (const component of components) { - const devfileRegistry = devfileRegistries.find( - (devfileRegistry) => format(devfileRegistry.url) === format(component.registry.url), - ); +// // const components = await ComponentTypesView.instance.getCompDescriptions(); +// const components = []; +// for (const component of components) { +// const devfileRegistry = devfileRegistries.find( +// (devfileRegistry) => format(devfileRegistry.url) === format(component.registry.url), +// ); - if (!component?.tags.some((value) => value.toLowerCase().includes('deprecate'))) { - devfileRegistry.devfiles.push({ - description: component.description, - registryName: devfileRegistry.name, - logoUrl: component.devfileData.devfile.metadata.icon, - name: component.displayName, - id: component.name, - starterProjects: component.devfileData.devfile.starterProjects, - tags: component.tags, - yaml: JSYAML.dump(component.devfileData.devfile), - supportsDebug: - Boolean( - component.devfileData.devfile.commands?.find( - (command) => command.exec?.group?.kind === 'debug', - ), - ) || - Boolean( - component.devfileData.devfile.commands?.find( - (command) => command.composite?.group?.kind === 'debug', - ), - ), - supportsDeploy: - Boolean( - component.devfileData.devfile.commands?.find( - (command) => command.exec?.group?.kind === 'deploy', - ), - ) || - Boolean( - component.devfileData.devfile.commands?.find( - (command) => command.composite?.group?.kind === 'deploy', - ), - ), - } as Devfile); - } - } - devfileRegistries.sort((a, b) => (a.name < b.name ? -1 : 1)); - return devfileRegistries; -} +// if (!component?.tags.some((value) => value.toLowerCase().includes('deprecate'))) { +// devfileRegistry.devfiles.push({ +// description: component.description, +// registryName: devfileRegistry.name, +// logoUrl: component.devfileData.devfile.metadata.icon, +// name: component.displayName, +// id: component.name, +// starterProjects: component.devfileData.devfile.starterProjects, +// tags: component.tags, +// yaml: JSYAML.dump(component.devfileData.devfile), +// supportsDebug: +// Boolean( +// component.devfileData.devfile.commands?.find( +// (command) => command.exec?.group?.kind === 'debug', +// ), +// ) || +// Boolean( +// component.devfileData.devfile.commands?.find( +// (command) => command.composite?.group?.kind === 'debug', +// ), +// ), +// supportsDeploy: +// Boolean( +// component.devfileData.devfile.commands?.find( +// (command) => command.exec?.group?.kind === 'deploy', +// ), +// ) || +// Boolean( +// component.devfileData.devfile.commands?.find( +// (command) => command.composite?.group?.kind === 'deploy', +// ), +// ), +// } as Devfile); +// } +// } +// devfileRegistries.sort((a, b) => (a.name < b.name ? -1 : 1)); +// return devfileRegistries; +// } + +// export async function getDevfileRawInfo(name: string, registryName: string): Promise { +// return await Odo.Instance.getDetailedComponentInformation( {name, registryName} as ComponentTypeAdapter); +// } /** * Returns a list of possible the devfile capabilities. @@ -261,15 +264,62 @@ export function getDevfileCapabilities(): string[] { * * @returns a list of the devfile tags */ -export async function getDevfileTags(url?: string): Promise { - const devfileRegistries = await getDevfileRegistries(); +export async function getDevfileTags(registryUrl?: string): Promise { + const devfileRegistries = await DevfileRegistry.Instance.getRegistryDevfileInfos(registryUrl); const devfileTags: string[] = [ ...new Set( devfileRegistries - .filter((devfileRegistry) => url ? devfileRegistry.url === url : true) - .flatMap((_devfileRegistry) => _devfileRegistry.devfiles) + .filter((devfileRegistry) => registryUrl ? devfileRegistry.registry.url === registryUrl : true) .flatMap((_devfile) => _devfile.tags)) ] return devfileTags.filter((devfileTag) => !devfileTag.toLowerCase().includes('deprecate')); } + +export async function sendUpdatedRegistries(panel: WebviewPanel, registryUrl?: string) { + if (panel) { + const registries = await DevfileRegistry.Instance.getRegistries(registryUrl) + void panel.webview.postMessage({ + action: 'devfileRegistries', + data: registries, + }); + } +} + +export function sendUpdatedCapabilities(panel: WebviewPanel) { + if (panel) { + void panel.webview.postMessage({ + action: 'devfileCapabilities', + data: getDevfileCapabilities(), + }); + } +} + +export async function sendUpdatedTags(panel: WebviewPanel, registryUrl?: string) { + if (panel) { + void panel.webview.postMessage({ + action: 'devfileTags', + data: await getDevfileTags(registryUrl), + }); + } +} + +export async function sendUpdatedDevfileInfos(panel: WebviewPanel, registryUrl?: string) { + if (panel) { + const devfileInfos = await DevfileRegistry.Instance.getRegistryDevfileInfos(registryUrl); + void panel.webview.postMessage({ + action: 'devfileInfos', + data: devfileInfos, + }); + } +} + +export async function sendDevfileForVersion(panel: WebviewPanel, devfileInfo?: DevfileInfo, version?: string) { + if (panel && devfileInfo) { + const devfile = await DevfileRegistry.Instance.getRegistryDevfile(devfileInfo.registry.url, devfileInfo.name, version); + void panel.webview.postMessage({ + action: 'devfile', + data: devfile + }); + } +} \ No newline at end of file diff --git a/src/webview/common/createComponentButton.tsx b/src/webview/common/createComponentButton.tsx index 3f531a471..4ab91940e 100644 --- a/src/webview/common/createComponentButton.tsx +++ b/src/webview/common/createComponentButton.tsx @@ -9,6 +9,7 @@ import * as React from 'react'; export type CreateComponentButtonProps = { componentName: string; + devfileVersion: string; componentParentFolder: string; addToWorkspace: boolean; portNumber: number; @@ -16,7 +17,7 @@ export type CreateComponentButtonProps = { isPortNumberFieldValid: boolean; isFolderFieldValid: boolean; isLoading: boolean; - createComponent: (projectFolder: string, componentName: string, isAddToWorkspace: boolean, portNumber: number) => void; + createComponent: (projectFolder: string, componentName: string, componentVersion: string, isAddToWorkspace: boolean, portNumber: number) => void; setLoading: React.Dispatch>; }; @@ -29,7 +30,7 @@ export function CreateComponentButton(props: CreateComponentButtonProps) { { - props.createComponent(props.componentParentFolder, props.componentName, props.addToWorkspace, props.portNumber); + props.createComponent(props.componentParentFolder, props.componentName, props.devfileVersion, props.addToWorkspace, props.portNumber); props.setLoading(true); }} disabled={!props.isComponentNameFieldValid || !props.isPortNumberFieldValid || !props.isFolderFieldValid || props.isLoading} diff --git a/src/webview/common/devfile.ts b/src/webview/common/devfile.ts index 778c52801..1171a1048 100644 --- a/src/webview/common/devfile.ts +++ b/src/webview/common/devfile.ts @@ -27,6 +27,7 @@ export type DevfileRegistry = { export type TemplateProjectIdentifier = { devfileId: string; + devfileVersion: string; registryName: string; templateProjectName: string; }; diff --git a/src/webview/common/devfileListItem.tsx b/src/webview/common/devfileListItem.tsx index 759c8af8e..f233e7486 100644 --- a/src/webview/common/devfileListItem.tsx +++ b/src/webview/common/devfileListItem.tsx @@ -5,12 +5,13 @@ import { Check } from '@mui/icons-material'; import { Box, Chip, Stack, Tooltip, Typography } from '@mui/material'; import * as React from 'react'; -import { Devfile } from '../common/devfile'; +import validator from 'validator'; import DevfileLogo from '../../../images/context/devfile.png'; -import validator from 'validator' +import { DevfileData, DevfileInfo } from '../../devfile-registry/devfileInfo'; export type DevfileListItemProps = { - devfile: Devfile; + devfileInfo?: DevfileInfo; + devfile: DevfileData; buttonCallback?: () => void; showFullDescription?: boolean; }; @@ -37,6 +38,7 @@ export function DevfileListItem(props: DevfileListItemProps) { }} > ) : ( <> - + )} @@ -54,6 +60,17 @@ export function DevfileListItem(props: DevfileListItemProps) { function DevfileListContent(props: DevfileListItemProps) { // for the width setting: // one unit of padding is 8px with the default MUI theme, and we add a margin on both sides + + const icon = checkedDevfileLogoUrl(props.devfile?.metadata?.icon ? + props.devfile.metadata.icon : props.devfileInfo?.icon); + const name = props.devfile?.metadata?.displayName ? props.devfile.metadata.displayName : props.devfileInfo?.displayName; + const version = props.devfile?.metadata?.version ? props.devfile.metadata.version : undefined; + const registryName = props.devfileInfo?.registry?.name; + const isDebugSupported = props.devfileInfo?.versions?.some((version) => version.commandGroups.debug === true); + const isDeploySupported = props.devfileInfo?.versions?.some((version) => version.commandGroups.deploy === true); + const tags = props.devfileInfo?.tags; + const description = props.devfile?.metadata?.description ? + props.devfile.metadata.description : props.devfileInfo?.description; return ( - @@ -83,47 +100,50 @@ function DevfileListContent(props: DevfileListItemProps) { - {props.devfile.name} + {name}{ version && `, v. ${version}`} - {props.devfile.registryName && ( - - from {props.devfile.registryName} - + { + registryName && ( + + from {registryName} + )} { - props.devfile.supportsDebug && } - color={'success'} - /> + isDebugSupported && + } + color={'success'} + /> } { - props.devfile.supportsDeploy && } - color={'success'} - /> + isDeploySupported && + } + color={'success'} + /> } - {(props.devfile.tags && props.devfile.tags.map((tag, i) => { + {(tags && tags.map((tag, i) => { if (i >= 4) { return; } return ; }))} - {(props.devfile.tags && props.devfile.tags.length > 4 && ( - + {(tags && tags.length > 4 && ( + ))} @@ -140,7 +160,7 @@ function DevfileListContent(props: DevfileListItemProps) { maxHeight: !props.showFullDescription ? '4rem' : 'unset' }} > - {props.devfile.description} + {description} diff --git a/src/webview/common/devfileSearch.tsx b/src/webview/common/devfileSearch.tsx index ca786d6b9..b9fb159ce 100644 --- a/src/webview/common/devfileSearch.tsx +++ b/src/webview/common/devfileSearch.tsx @@ -2,6 +2,7 @@ * Copyright (c) Red Hat, Inc. All rights reserved. * Licensed under the MIT License. See LICENSE file in the project root for license information. *-----------------------------------------------------------------------------------------------*/ +import { yaml } from '@codemirror/lang-yaml'; import { Close, FileCopy, Launch, Search } from '@mui/icons-material'; import { Box, @@ -28,16 +29,16 @@ import { Typography, useMediaQuery } from '@mui/material'; +import { vscodeDarkInit, vscodeLightInit } from '@uiw/codemirror-theme-vscode'; +import CodeMirror from '@uiw/react-codemirror'; import { every } from 'lodash'; import * as React from 'react'; import CopyToClipboard from 'react-copy-to-clipboard'; -import { Devfile, DevfileRegistry, TemplateProjectIdentifier } from '../common/devfile'; +import { DevfileData, DevfileInfo, DevfileRegistryInfo } from '../../devfile-registry/devfileInfo'; +import { TemplateProjectIdentifier } from '../common/devfile'; import { DevfileExplanation } from './devfileExplanation'; import { DevfileListItem } from './devfileListItem'; import { LoadScreen } from './loading'; -import CodeMirror from '@uiw/react-codemirror'; -import { yaml } from '@codemirror/lang-yaml'; -import { vscodeDarkInit, vscodeLightInit } from '@uiw/codemirror-theme-vscode'; // in order to add custom named colours for use in Material UI's `color` prop, // you need to use module augmentation. @@ -286,25 +287,65 @@ function TagsPicker(props: { const SelectTemplateProject = React.forwardRef( ( props: { - devfile: Devfile; + devfileInfo: DevfileInfo; + selectedDevfileVersion?: string; + setSelectedDevfile: (selected: DevfileData) => void; setSelectedProject: (projectName: string) => void; closeModal: () => void; theme: Theme; } ) => { + const [isSomeDevfileRetrieved, setSomeDevfileRetrieved] = React.useState(false); + const [selectedDevfileVersion, setSelectedDevfileVersion] = React.useState(undefined); + const [selectedDevfile, setSelectedDevfile] = React.useState(undefined); const [selectedTemplateProject, setSelectedTemplateProject] = React.useState(''); const [isInteracted, setInteracted] = React.useState(false); const [isYamlCopied, setYamlCopied] = React.useState(false); - const isWideEnough = useMediaQuery('(min-width: 900px)'); + function respondToMessage(messageEvent: MessageEvent) { + const message = messageEvent.data as Message; + switch (message.action) { + case 'devfile': { + if (message.data) { + const devfile = message.data; + setSelectedDevfileVersion((_version) => devfile.metadata?.version); + setSelectedDevfile((_devfile) => devfile); + setSomeDevfileRetrieved(_unused => true); + } + break; + } + default: + break; + } + } React.useEffect(() => { - if (props.devfile.starterProjects && props.devfile.starterProjects.length > 0) { - setSelectedTemplateProject((_) => props.devfile.starterProjects[0].name); - } + window.addEventListener('message', respondToMessage); + return () => { + window.removeEventListener('message', respondToMessage); + }; }, []); - const starterProjects = props.devfile.starterProjects ? props.devfile.starterProjects : []; + React.useEffect(() => { + window.vscodeApi.postMessage( + { + action: 'getDevfile', + data: { + devfileInfo: props.devfileInfo, + version: selectedDevfileVersion + } + }); + }, []); + + React.useEffect(() => { + if (selectedDevfile) { + if (selectedDevfile.starterProjects && selectedDevfile.starterProjects.length > 0) { + setSelectedTemplateProject((_) => selectedDevfile.starterProjects[0].name); + } + } + }, [selectedDevfile]); + + const starterProjects = selectedDevfile?.starterProjects ? selectedDevfile.starterProjects : []; let helperText = ''; switch (starterProjects.length) { case 0: @@ -320,6 +361,29 @@ const SelectTemplateProject = React.forwardRef( break; } + const versions = props.devfileInfo.versions ? props.devfileInfo.versions : []; + const initialSelectedVersion = selectedDevfileVersion + || props.devfileInfo.versions.filter((versionInfo) => versionInfo.default).pop()?.version + || props.devfileInfo.versions.pop()?.version; + if (!selectedDevfileVersion) { + setSelectedDevfileVersion(initialSelectedVersion); + } + + let versionHelperText = ''; + switch (versions.length) { + case 0: + versionHelperText = 'No available versions'; + break; + case 1: + versionHelperText = 'Only one version is available for the Devfile'; + break; + default: + if (isInteracted && !selectedDevfileVersion) { + versionHelperText = 'Select a version'; + } + break; + } + const projectUrl = React.useMemo(() => { if (!selectedTemplateProject) { return undefined; @@ -337,7 +401,11 @@ const SelectTemplateProject = React.forwardRef( return fullSelectedTemplateProject.zip.location; } return undefined; - }, [selectedTemplateProject]); + }, [selectedDevfile, selectedTemplateProject]); + + const isWideEnough = useMediaQuery('(min-width: 900px)'); + + const isVersionError = !props.devfileInfo.versions?.length || (isInteracted && !selectedDevfileVersion); const isError = !starterProjects.length || (isInteracted && !selectedTemplateProject); @@ -354,124 +422,179 @@ const SelectTemplateProject = React.forwardRef( padding: 2, }} > - - - - - - + { + !isSomeDevfileRetrieved ? + + - - Template Project - - - {helperText} - - { + + + + + + + + Version + + + + {versionHelperText} + + + + + Template Project + + + + {helperText} + + { + window.vscodeApi.postMessage({ + action: 'sendTelemetry', + data: { + actionName: 'devfileSearchOpenProjectInBrowser', + properties: { + // eslint-disable-next-line camelcase + component_type: props.devfileInfo.name, + // eslint-disable-next-line camelcase + starter_project: selectedTemplateProject, + }, + }, + }); + }} + > + Open Project in Browser + + + + + + + + { window.vscodeApi.postMessage({ action: 'sendTelemetry', data: { - actionName: 'devfileSearchOpenProjectInBrowser', + actionName: 'devfileSearchCopiedYaml', properties: { // eslint-disable-next-line camelcase - component_type: props.devfile.name, + component_type: props.devfileInfo.name, // eslint-disable-next-line camelcase starter_project: selectedTemplateProject, }, }, }); + setYamlCopied((_) => true); }} > - Open Project in Browser - - - - - - - - { - window.vscodeApi.postMessage({ - action: 'sendTelemetry', - data: { - actionName: 'devfileSearchCopiedYaml', - properties: { - // eslint-disable-next-line camelcase - component_type: props.devfile.name, - // eslint-disable-next-line camelcase - starter_project: selectedTemplateProject, - }, - }, - }); - setYamlCopied((_) => true); - }} - > - { - setTimeout(() => setYamlCopied((_) => false), 200); - }} - arrow - > - - - - - + { + setTimeout(() => setYamlCopied((_) => false), 200); + }} + arrow + > + + + + + + + - - - + + } ); }, @@ -480,13 +603,21 @@ const SelectTemplateProject = React.forwardRef( export type DevfileSearchProps = { titleText: string; + /** + * The callback to run when the user selects a DevfileInfo. + * + * In order to avoid showing the template project selector, + * write a callback that removes the DevfileSearch component from the page. + */ + setSelectedDevfileInfo?: (selected: DevfileInfo) => void; + /** * The callback to run when the user selects a Devfile. * * In order to avoid showing the template project selector, * write a callback that removes the DevfileSearch component from the page. */ - setSelectedDevfile?: (selected: Devfile) => void; + setSelectedDevfile?: (selected: DevfileData) => void; /** * The callback to run when the user selects a template project. @@ -509,22 +640,30 @@ export type DevfileSearchProps = { * * @returns true if the specified Devfile should be included in the search results, and false otherwise */ -function isToBeIncluded(devfile: Devfile, tagFilter: string[], debugSupportFilter: boolean, deploySupportFilter: boolean): boolean { - const includesDebugSupport = debugSupportFilter === false || debugSupportFilter === devfile.supportsDebug; - const includesDeploySupport = deploySupportFilter === false || deploySupportFilter === devfile.supportsDeploy; - const includesTags = tagFilter.length === 0 || devfile.tags.filter((_devfileTag) => { +function isToBeIncluded(devfileInfo: DevfileInfo, tagFilter: string[], debugSupportFilter: boolean, deploySupportFilter: boolean): boolean { + const includesDebugSupport = debugSupportFilter === false || + devfileInfo.versions.some((version) => version.commandGroups.debug === debugSupportFilter); + const includesDeploySupport = deploySupportFilter === false || + devfileInfo.versions.some((version) => version.commandGroups.deploy === deploySupportFilter); + const includesTags = tagFilter.length === 0 || devfileInfo.tags.filter((_devfileTag) => { return tagFilter.find((_selectedTags) => _devfileTag === _selectedTags) !== undefined; }).length > 0; return includesDebugSupport && includesDeploySupport && includesTags; } +function getDefaultDevfileVersion(devfileInfo: DevfileInfo): string { + return devfileInfo.versions.find((_versionInfo) => _versionInfo.default)?.version || 'latest'; +} + export function DevfileSearch(props: DevfileSearchProps) { const ITEMS_PER_PAGE = 12; - const [selectedDevfile, setSelectedDevfile] = React.useState(); + const [isSomeDevfileInfoRetrieved, setSomeDevfileInfoRetrieved] = React.useState(false); + const [selectedDevfileInfo, setSelectedDevfileInfo] = React.useState(); const [currentPage, setCurrentPage] = React.useState(1); - const [devfileRegistries, setDevfileRegistries] = React.useState([]); + const [devfileRegistries, setDevfileRegistries] = React.useState([]); + const [devfileInfos, setDevfileInfos] = React.useState([]); const [registryEnabled, setRegistryEnabled] = React.useState< { registryName: string; registryUrl: string; enabled: boolean }[] >([]); @@ -547,6 +686,11 @@ export function DevfileSearch(props: DevfileSearchProps) { setDevfileRegistries((_devfileRegistries) => message.data); break; } + case 'devfileInfos': { + setDevfileInfos((_devfileInfos) => message.data); + setSomeDevfileInfoRetrieved(_unused => true); + break; + } case 'devfileCapabilities': { setDevfileCapabilities((_devfileCapabilities) => message.data); break; @@ -597,8 +741,10 @@ export function DevfileSearch(props: DevfileSearchProps) { React.useEffect(() => clearDevfileAll(), [devfileTags]); React.useEffect(() => { - props.setSelectedDevfile(selectedDevfile); - }, [selectedDevfile]); + if (props.setSelectedDevfileInfo) { + props.setSelectedDevfileInfo(selectedDevfileInfo); + } + }, [selectedDevfileInfo]); React.useEffect(() => { window.addEventListener('message', respondToMessage); @@ -611,6 +757,10 @@ export function DevfileSearch(props: DevfileSearchProps) { window.vscodeApi.postMessage({ action: 'getDevfileRegistries' }); }, []); + React.useEffect(() => { + window.vscodeApi.postMessage({ action: 'getDevfileInfos' }); + }, []); + React.useEffect(() => { window.vscodeApi.postMessage({ action: 'getDevfileCapabilities' }); }, []); @@ -623,16 +773,16 @@ export function DevfileSearch(props: DevfileSearchProps) { setCurrentPage((_) => 1); }, [registryEnabled, capabilityEnabled, tagEnabled, searchText]); - if (!devfileRegistries) { - return ; + if (!devfileInfos) { + return ; } if (!devfileCapabilities) { - return ; + return ; } if (!devfileTags) { - return ; + return ; } const activeRegistries = registryEnabled // @@ -653,17 +803,16 @@ export function DevfileSearch(props: DevfileSearchProps) { .filter((_tag) => _tag.enabled) // .map((_tag) => _tag.name); - const devfiles: Devfile[] = devfileRegistries // - .filter((devfileRegistry) => activeRegistries.includes(devfileRegistry.name)) // - .flatMap((devfileRegistry) => devfileRegistry.devfiles) // - .filter((devfile) => isToBeIncluded(devfile, activeTags, debugSupport, deploySupport)) // - .filter((devfile) => { + const devfiles: DevfileInfo[] = devfileInfos // + .filter((devfileInfo) => activeRegistries.includes(devfileInfo.registry.name)) // + .filter((devfileInfo) => isToBeIncluded(devfileInfo, activeTags, debugSupport, deploySupport)) // + .filter((devfileInfo) => { const searchTerms = searchText.split(/\s+/); return every( searchTerms.map( (searchTerm) => - devfile.name.toLowerCase().includes(searchTerm) || - devfile.tags.find((tag) => tag.toLowerCase().includes(searchTerm)), + devfileInfo.name.toLowerCase().includes(searchTerm) || + devfileInfo.tags.find((tag) => tag.toLowerCase().includes(searchTerm)), ), ); }); @@ -677,161 +826,168 @@ export function DevfileSearch(props: DevfileSearchProps) { return 1; } - if (a.supportsDebug && !b.supportsDebug) { + const aSupportsDebug = a.versions.some((version) => version.commandGroups.debug === true); + const bSupportsDebug = b.versions.some((version) => version.commandGroups.debug === true); + + if (aSupportsDebug && !bSupportsDebug) { return -1; - } else if (b.supportsDebug && !a.supportsDebug) { + } else if (bSupportsDebug && !aSupportsDebug) { return 1; } - return a.name < b.name ? -1 : 1; + return a.displayName < b.displayName ? -1 : 1; }); return ( <> - - - - Filter by - - - { - devfileRegistries.length > 1 && ( - <> - - Devfile Registries - - - - - ) - } + { + !isSomeDevfileInfoRetrieved ? + : + + + + Filter by + - { - devfileCapabilities.length > 0 && ( - - - Support - - - { - devfileCapabilities.length > 0 && ( - <> - - - - ) - } - - - ) - } - - { - devfileTags.length > 0 && ( - <> - - - Tags - - - - - - { - setShowMore((prev) => !prev); - if (showMore) { - const myDiv = document.getElementById('tags'); - myDiv.scrollTop = 0; - } - }} - > - Show {!showMore ? 'more' : 'less'} - - - { - activeTags.length > 0 && + { + devfileRegistries.length > 1 && ( + <> - { - clearDevfileAll() - }} - > - Clear {activeTags.length > 1 ? 'all' : ''} - + Devfile Registries - } - - - ) - } - + + + + ) + } - - - - 0.0001 ? 1 : 0) - } - perPageCount={ITEMS_PER_PAGE} - devfilesLength={devfiles.length} - /> - {/* 320px is the approximate combined height of the top bar and bottom bar in the devfile search view */} - {/* 5em is the padding at the top of the page */} - } - width={'100%'} - > - {devfiles - .slice( - (currentPage - 1) * ITEMS_PER_PAGE, - Math.min(currentPage * ITEMS_PER_PAGE, devfiles.length), - ) - .map((devfile) => { - return ( - { - setSelectedDevfile(devfile); - }} - /> - ); - })} + { + devfileCapabilities.length > 0 && ( + + + Support + + + { + devfileCapabilities.length > 0 && ( + <> + + + + ) + } + + + ) + } + + { + devfileTags.length > 0 && ( + <> + + + Tags + + + + + + { + setShowMore((prev) => !prev); + if (showMore) { + const myDiv = document.getElementById('tags'); + myDiv.scrollTop = 0; + } + }} + > + Show {!showMore ? 'more' : 'less'} + + + { + activeTags.length > 0 && + + { + clearDevfileAll() + }} + > + Clear {activeTags.length > 1 ? 'all' : ''} + + + } + + + ) + } + + + + + 0.0001 ? 1 : 0) + } + perPageCount={ITEMS_PER_PAGE} + devfilesLength={devfiles.length} + /> + {/* 320px is the approximate combined height of the top bar and bottom bar in the devfile search view */} + {/* 5em is the padding at the top of the page */} + } + width={'100%'} + > + {devfiles + .slice( + (currentPage - 1) * ITEMS_PER_PAGE, + Math.min(currentPage * ITEMS_PER_PAGE, devfiles.length), + ) + .map((devfileInfo) => { + return ( + { + setSelectedDevfileInfo(devfileInfo); + }} + /> + ); + })} + + - - + } {props.goBack && ( @@ -848,25 +1004,33 @@ export function DevfileSearch(props: DevfileSearchProps) { { - setSelectedDevfile(undefined); + setSelectedDevfileInfo(undefined); }} - open={!!selectedDevfile} + open={!!selectedDevfileInfo} disableScrollLock > { - if (!selectedDevfile) { + if (!selectedDevfileInfo) { return; } - props.setSelectedTemplateProject({ - devfileId: selectedDevfile.id, - registryName: selectedDevfile.registryName, - templateProjectName: projectName, - }); + if (props.setSelectedTemplateProject) { + props.setSelectedTemplateProject({ + devfileId: selectedDevfileInfo.name, + devfileVersion: getDefaultDevfileVersion(selectedDevfileInfo), + registryName: selectedDevfileInfo.registry.name, + templateProjectName: projectName, + }); + } + }} + setSelectedDevfile={(devfile) => { + if (props.setSelectedDevfile) { + props.setSelectedDevfile(devfile); + } }} closeModal={() => { - setSelectedDevfile((_) => undefined); + setSelectedDevfileInfo((_) => undefined); }} theme={props.theme} /> diff --git a/src/webview/common/fromTemplateProject.tsx b/src/webview/common/fromTemplateProject.tsx index 179c0680f..96afec3c5 100644 --- a/src/webview/common/fromTemplateProject.tsx +++ b/src/webview/common/fromTemplateProject.tsx @@ -2,12 +2,13 @@ * Copyright (c) Red Hat, Inc. All rights reserved. * Licensed under the MIT License. See LICENSE file in the project root for license information. *-----------------------------------------------------------------------------------------------*/ +import { Theme } from '@mui/material'; import * as React from 'react'; import 'react-dom'; -import { Devfile, TemplateProjectIdentifier } from './devfile'; +import { DevfileData, DevfileInfo } from '../../devfile-registry/devfileInfo'; +import { TemplateProjectIdentifier } from './devfile'; import { DevfileSearch } from './devfileSearch'; import { SetNameAndFolder } from './setNameAndFolder'; -import { Theme } from '@mui/material'; type CurrentPage = 'selectTemplateProject' | 'setNameAndFolder'; @@ -26,7 +27,9 @@ export function FromTemplateProject(props: FromTemplateProjectProps) { const [currentPage, setCurrentPage] = React.useState('selectTemplateProject'); const [selectedTemplateProject, setSelectedTemplateProject] = React.useState(undefined); - const [selectedDevfile, setSelectedDevfile] = React.useState(undefined); + const [selectedDevfileInfo, setSelectedDevfileInfo] = React.useState(undefined); + const [selectedDevfile, setSelectedDevfile] = React.useState(undefined); + const [selectedDevfileVersion, setSelectedDevfileVersion] = React.useState(undefined); const [initialComponentParentFolder, setInitialComponentParentFolder] = React.useState(undefined); function respondToMessage(messageEvent: MessageEvent) { @@ -36,6 +39,10 @@ export function FromTemplateProject(props: FromTemplateProjectProps) { setInitialComponentParentFolder(message.data); break; } + case 'devfile': { + setSelectedDevfile((_devfile) => message.data.devfile); + break; + } default: break; } @@ -52,18 +59,34 @@ export function FromTemplateProject(props: FromTemplateProjectProps) { window.vscodeApi.postMessage({ action: 'getInitialWokspaceFolder' }); }, []); + React.useEffect(() => { + window.vscodeApi.postMessage({ + action: 'getDevfile', + data: { + devfile: selectedDevfileInfo, + version: selectedDevfileVersion + } + }); + }, []); + + React.useEffect(() => { + setSelectedDevfileVersion((_) => selectedDevfileInfo?.versions.find((v) => v.default)?.version); + }, [selectedDevfileInfo]); + function setSelectedProjectAndAdvance(value: TemplateProjectIdentifier) { + value.devfileVersion = selectedDevfileVersion; // Update selected version setSelectedTemplateProject((_) => value); setCurrentPage((_) => 'setNameAndFolder'); } - function createComponent(projectFolder: string, componentName: string, addToWorkspace: boolean, portNumber: number) { + function createComponent(projectFolder: string, componentName: string, devfileVersion: string, addToWorkspace: boolean, portNumber: number) { window.vscodeApi.postMessage({ action: 'createComponent', data: { templateProject: selectedTemplateProject, projectFolder, componentName, + devfileVersion, portNumber, isFromTemplateProject: true, addToWorkspace @@ -75,6 +98,7 @@ export function FromTemplateProject(props: FromTemplateProjectProps) { case 'selectTemplateProject': return ( void; - createComponent: (projectFolder: string, componentName: string, addToWorkspace: boolean, portNumber: number) => void; - devfile: Devfile; + createComponent: (projectFolder: string, componentName: string, componentVersion: string, addToWorkspace: boolean, portNumber: number) => void; + devfileInfo: DevfileInfo; + devfile: DevfileData; templateProject?: string; initialComponentName?: string; initialComponentParentFolder?: string; }; +const getTargetPort = ((devfile: DevfileData, templateProject: string): number => { + const component = devfile.components.find((component) => component.name === templateProject); + if (component && devfile?.components[0]?.container?.endpoints[0]) { + return component.container.endpoints[0].targetPort; + } + + // Find first existing component container with a targetPort defined in its endpoint + return devfile.components.filter((component) => component.container) + .filter((component) => component.container.endpoints && component.container?.endpoints[0]) + .map((component) => component.container?.endpoints[0].targetPort) + .pop() | 0; +}); + +const getComponentVersion = ((devfile: DevfileData): string => { + return devfile.metadata?.version ? devfile.metadata.version : 'latest'; +}); + export function SetNameAndFolder(props: SetNameAndFolderProps) { const [componentName, setComponentName] = React.useState(props.initialComponentName); - const [portNumber, setPortNumber] = React.useState(props.devfile.port); + const [portNumber, setPortNumber] = React.useState(getTargetPort(props.devfile, props.templateProject)); const [isComponentNameFieldValid, setComponentNameFieldValid] = React.useState(true); const [componentNameErrorMessage, setComponentNameErrorMessage] = React.useState( 'Please enter a component name.', @@ -139,10 +157,10 @@ export function SetNameAndFolder(props: SetNameAndFolderProps) { }, [componentName]); React.useEffect(() => { - if (props.devfile.port) { + if (props.devfile) { window.vscodeApi.postMessage({ action: 'validatePortNumber', - data: `${props.devfile.port}`, + data: `${getTargetPort(props.devfile, props.templateProject)}`, }); } }, []); @@ -155,9 +173,9 @@ export function SetNameAndFolder(props: SetNameAndFolderProps) { {props.devfile ? ( - + - + {/* padding here is to match the padding build into the devfile list component */} {props.templateProject && ( @@ -177,7 +195,7 @@ export function SetNameAndFolder(props: SetNameAndFolderProps) { setComponentName={setComponentName} /> { - portNumber && + portNumber !== undefined && { if (CreateComponentLoader.panel) { CreateComponentLoader.panel.reveal(); - return; + return CreateComponentLoader.panel; } const localResourceRoot = Uri.file( path.join(CreateComponentLoader.extensionPath, 'out', 'create-component'), @@ -79,23 +82,23 @@ export default class CreateComponentLoader { await panel.webview.postMessage({ action: 'setTheme', themeValue: colorTheme.kind }); }); - const registriesSubscription = ComponentTypesView.instance.subject.subscribe(() => { - void sendUpdatedRegistries(); - }); + // const registriesSubscription = ComponentTypesView.instance.subject.subscribe(() => { + // void sendUpdatedRegistries(); + // }); - const capabiliiesySubscription = ComponentTypesView.instance.subject.subscribe(() => { - sendUpdatedCapabilities(); - }); + // const capabiliiesySubscription = ComponentTypesView.instance.subject.subscribe(() => { + // sendUpdatedCapabilities(); + // }); - const tagsSubscription = ComponentTypesView.instance.subject.subscribe(() => { - void sendUpdatedTags(); - }); + // const tagsSubscription = ComponentTypesView.instance.subject.subscribe(() => { + // void sendUpdatedTags(); + // }); panel.onDidDispose(() => { void sendTelemetry('newComponentClosed'); - tagsSubscription.unsubscribe(); - capabiliiesySubscription.unsubscribe(); - registriesSubscription.unsubscribe(); + // tagsSubscription.unsubscribe(); + // capabiliiesySubscription.unsubscribe(); + // registriesSubscription.unsubscribe(); colorThemeDisposable.dispose(); messageHandlerDisposable.dispose(); CreateComponentLoader.panel = undefined; @@ -135,33 +138,36 @@ export default class CreateComponentLoader { break; } /** - * The panel requested the list of devfile registries with their devfiles. Respond with this list. + * The panel requested the list of devfile registries. Respond with this list. */ case 'getDevfileRegistries': { - void CreateComponentLoader.panel.webview.postMessage({ - action: 'devfileRegistries', - data: await getDevfileRegistries(), - }); + await sendUpdatedRegistries(CreateComponentLoader.panel); break; } + /** + * The panel requested the list of devfile info. Respond with this list. + */ + case 'getDevfileInfos': + await sendUpdatedDevfileInfos(CreateComponentLoader.panel); + break; + /** + * The panel requested the devfile of specified version. Respond with this data. + */ + case 'getDevfile': + await sendDevfileForVersion(CreateComponentLoader.panel, message.data?.devfileInfo, message.data?.version); + break; /** * The panel requested the list of devfile capabilities. Respond with this list. */ case 'getDevfileCapabilities': { - void CreateComponentLoader.panel.webview.postMessage({ - action: 'devfileCapabilities', - data: getDevfileCapabilities(), - }); + sendUpdatedCapabilities(CreateComponentLoader.panel); break; } /** * The panel requested the list of devfile tags. Respond with this list. */ case 'getDevfileTags': { - void CreateComponentLoader.panel.webview.postMessage({ - action: 'devfileTags', - data: await getDevfileTags(), - }); + await sendUpdatedTags(CreateComponentLoader.panel); break; } /** @@ -266,7 +272,7 @@ export default class CreateComponentLoader { break; } /** - * The panel requested to get the receommended devfile given the selected project. + * The panel requested to get the recommended devfile given the selected project. */ case 'getRecommendedDevfile': { await CreateComponentLoader.panel.webview.postMessage({ @@ -324,6 +330,8 @@ export default class CreateComponentLoader { */ case 'createComponent': { const componentName: string = message.data.componentName; + const devfileVersion = message.data.devfileVersion && message.data.devfileVersion.length > 0 ? + message.data.devfileVersion : 'latest'; const portNumber: number = message.data.portNumber; let componentFolder: string = ''; try { @@ -339,6 +347,7 @@ export default class CreateComponentLoader { componentName, portNumber, templateProject.devfileId, + devfileVersion, templateProject.registryName, templateProject.templateProjectName, ); @@ -366,11 +375,13 @@ export default class CreateComponentLoader { await fs.mkdir(componentFolder, {recursive: true}); await fse.copy(tmpFolder.fsPath, componentFolder); } - const devfileType = await getDevfileType(message.data.devfileDisplayName); + const devfileInfo = await getDevfileInfoByDisplayName(message.data.devfileDisplayName); + const devfileType = devfileInfo ? devfileInfo.name : message.data.devfileDisplayName; const componentFolderUri = Uri.file(componentFolder); if (!await isDevfileExists(componentFolderUri)) { await Odo.Instance.createComponentFromLocation( devfileType, + devfileVersion, componentName, portNumber, Uri.file(componentFolder), @@ -521,13 +532,13 @@ export default class CreateComponentLoader { static async getRecommendedDevfile(uri: Uri): Promise { let analyzeRes: AlizerDevfileResponse; - let compDescriptions: ComponentTypeDescription[] = []; + let compDescriptions: DevfileInfoExt[] = []; try { void CreateComponentLoader.panel.webview.postMessage({ action: 'getRecommendedDevfileStart' }); const alizerAnalyzeRes: AlizerDevfileResponse = await Alizer.Instance.alizerDevfile(uri); - compDescriptions = await getCompDescription(alizerAnalyzeRes); + compDescriptions = await getCompDescriptionsAfterAnalizer(alizerAnalyzeRes); } catch (error) { if (error.message.toLowerCase().indexOf('failed to parse the devfile') !== -1) { const actions: Array = ['Yes', 'Cancel']; @@ -542,8 +553,8 @@ export default class CreateComponentLoader { const devfileV1 = JSYAML.load(file.toString()) as DevfileV1; await fs.unlink(devFileV1Path); analyzeRes = await Alizer.Instance.alizerDevfile(uri); - compDescriptions = await getCompDescription(analyzeRes); - const endPoints = getEndPoints(compDescriptions[0]); + compDescriptions = await getCompDescriptionsAfterAnalizer(analyzeRes); + const endPoints = await getEndPoints(compDescriptions[0]); const devfileV2 = DevfileConverter.getInstance().devfileV1toDevfileV2( devfileV1, endPoints, @@ -568,60 +579,88 @@ export default class CreateComponentLoader { void CreateComponentLoader.panel.webview.postMessage({ action: 'getRecommendedDevfile' }); - const devfileRegistry: DevfileRegistry[] = await getDevfileRegistries(); - const allDevfiles: Devfile[] = devfileRegistry.flatMap((registry) => registry.devfiles); - const devfile: Devfile | undefined = - compDescriptions.length !== 0 - ? allDevfiles.find( - (devfile) => devfile.name === compDescriptions[0].displayName, - ) - : undefined; - if (devfile) { - devfile.port = compDescriptions[0].devfileData.devfile.components[0].container?.endpoints[0].targetPort; - } + + const devfile = (!compDescriptions || compDescriptions.length === 0) ? undefined : + await DevfileRegistry.Instance.getRegistryDevfile(compDescriptions[0].registry.url, compDescriptions[0].name, compDescriptions[0].proposedVersion); + + const devfilePort = devfile?.components[0]?.container?.endpoints[0]?.targetPort; + void CreateComponentLoader.panel.webview.postMessage({ action: 'recommendedDevfile', data: { devfile, + port: devfilePort }, }); } } } -async function getCompDescription(devfile: AlizerDevfileResponse): Promise { - const compDescriptions = await ComponentTypesView.instance.getCompDescriptions(); - if (!devfile.Name) { - return Array.from(compDescriptions); - } - return Array.from(compDescriptions).filter((compDesc) => { - if (devfile.Name === compDesc.name && getVersion(devfile.Versions, compDesc.version)) { - return compDesc; +function findMostCommonVersion(devfileVersionInfos: DevfileVersionInfo[], analizerVersions: Version[]): string { + // Find Alizer's default version + const analizerDefaultVersion = analizerVersions.find((version) => version.Default)?.Version; + if (analizerDefaultVersion) { + const devfileVersion = devfileVersionInfos.find((versionInfo) => versionInfo.version === analizerDefaultVersion); + if (devfileVersion) { + return devfileVersion.version; } } - ); + + // Find most common latest version + const maxCommon = semver.sort(devfileVersionInfos.filter((_dfv) => analizerVersions.find((_av) => _dfv.version === _av.Version)) + .flatMap((_dfv) => _dfv.version))?.pop(); + return maxCommon; } -function getVersion(devfileVersions: Version[], matchedVersion: string): Version { - return devfileVersions.find((devfileVersion) => { - if (devfileVersion.Version === matchedVersion) { - return devfileVersion; - } +/** + * Returns an array of objects, that suites the criterias of Analize Responses provided, + * and include: DevfileInfo and according devfile version selected from an according analize response: + * ``` + * { + * ...devfileInfo: DevfileInfo, + * proposedVersion: string + * } + * ``` + * @param analizeResponse + * @returns Array of Devfile ibjects added with a Devfile version proposed by analizer + */ +async function getCompDescriptionsAfterAnalizer(analizeResponse: AlizerDevfileResponse): Promise { + const compDescriptions = await DevfileRegistry.Instance.getRegistryDevfileInfos(); + + if (!analizeResponse) { + return compDescriptions.flatMap((devfileInfo) => { + const defaultVersion = devfileInfo.versions?.find((version) => version.default).version + || devfileInfo.versions?.pop()?.version || undefined; + return { + ...devfileInfo, + proposedVersion: defaultVersion + } as DevfileInfoExt; + }); } - ); + + const devfileInfos = compDescriptions.filter((devfileInfo) => + devfileInfo.name === analizeResponse.Name && + findMostCommonVersion(devfileInfo.versions, analizeResponse.Versions) !== undefined) + .flatMap((devfileInfo) => { + return { + ...devfileInfo, + proposedVersion: findMostCommonVersion(devfileInfo.versions, analizeResponse.Versions) + } as DevfileInfoExt; + }); + + return devfileInfos; } -async function getDevfileType(devfileDisplayName: string): Promise { - const compDescriptions: Set = - await ComponentTypesView.instance.getCompDescriptions(); - const devfileDescription: ComponentTypeDescription = Array.from(compDescriptions).find( - (description) => description.displayName === devfileDisplayName, +async function getDevfileInfoByDisplayName(devfileDisplayName: string): Promise { + const devfileInfos = await DevfileRegistry.Instance.getRegistryDevfileInfos(); + return Array.from(devfileInfos).find( + (_info) => _info.displayName === devfileDisplayName ); - return devfileDescription ? devfileDescription.name : devfileDisplayName; } -function getEndPoints(compDescription: ComponentTypeDescription): Endpoint[] { - return compDescription.devfileData.devfile.components[0].container.endpoints; +async function getEndPoints(devfileInfoExt:DevfileInfoExt): Promise { + const devfile = await DevfileRegistry.Instance.getRegistryDevfile(devfileInfoExt.registry.url, devfileInfoExt.name, devfileInfoExt.proposedVersion); + return devfile?.components[0]?.container.endpoints; } async function isDevfileExists(uri: vscode.Uri): Promise { @@ -674,31 +713,4 @@ async function validateFolderPath(path: string) { }, }); } -} - -async function sendUpdatedRegistries() { - if (CreateComponentLoader.panel) { - void CreateComponentLoader.panel.webview.postMessage({ - action: 'devfileRegistries', - data: await getDevfileRegistries(), - }); - } -} - -function sendUpdatedCapabilities() { - if (CreateComponentLoader.panel) { - void CreateComponentLoader.panel.webview.postMessage({ - action: 'devfileCapabilities', - data: getDevfileCapabilities(), - }); - } -} - -async function sendUpdatedTags() { - if (CreateComponentLoader.panel) { - void CreateComponentLoader.panel.webview.postMessage({ - action: 'devfileTags', - data: await 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 4baac48e7..46b9a1775 100644 --- a/src/webview/create-component/pages/fromExistingGitRepo.tsx +++ b/src/webview/create-component/pages/fromExistingGitRepo.tsx @@ -16,13 +16,13 @@ import { Typography } from '@mui/material'; import * as React from 'react'; -import { Devfile } from '../../common/devfile'; +import { DevfileData } from '../../../devfile-registry/devfileInfo'; import { DevfileListItem } from '../../common/devfileListItem'; import { RecommendationInfo } from '../../common/devfileRecommendationInfo'; import { DevfileSearch } from '../../common/devfileSearch'; import { NoSuitableWarning } from '../../common/noSuitableDevfile'; -import { SetNameAndFolder } from '../../common/setNameAndFolder'; import { buildSanitizedComponentName } from '../../common/sanitize'; +import { SetNameAndFolder } from '../../common/setNameAndFolder'; type Message = { action: string; @@ -30,7 +30,7 @@ type Message = { }; type RecommendedDevfileState = { - devfile: Devfile; + devfile: DevfileData; showRecommendation: boolean; isLoading: boolean; completionValue: number; @@ -64,7 +64,7 @@ export function FromExistingGitRepo({ setCurrentView }) { isDevfileExistsInRepo: false, noRecommendation: false, }); - const [selectedDevfile, setSelectedDevfile] = React.useState(undefined); + const [selectedDevfile, setSelectedDevfile] = React.useState(undefined); const [initialComponentParentFolder, setInitialComponentParentFolder] = React.useState(undefined); function respondToMessage(messageEvent: MessageEvent) { @@ -158,7 +158,7 @@ export function FromExistingGitRepo({ setCurrentView }) { setRecommendedDevfile((prevState) => ({ ...prevState, isLoading: true, completionValue: 5 })); } - function getEffectiveDevfile() { + function getEffectiveDevfile(): DevfileData { return recommendedDevfile.isDevfileExistsInRepo ? recommendedDevfile.devfile // An existing Git-Repo devfile : selectedDevfile ? @@ -167,12 +167,13 @@ export function FromExistingGitRepo({ setCurrentView }) { } function getInitialComponentName() { - return getEffectiveDevfile()?.name; + return getEffectiveDevfile()?.metadata.name; } function createComponentFromGitRepo( projectFolder: string, componentName: string, + devfileVersion: string, addToWorkspace: boolean, portNumber: number ) { @@ -180,9 +181,10 @@ export function FromExistingGitRepo({ setCurrentView }) { action: 'createComponent', data: { devfileDisplayName: selectedDevfile - ? selectedDevfile.name - : recommendedDevfile.devfile.name, + ? selectedDevfile.metadata.name + : recommendedDevfile.devfile.metadata.name, componentName, + devfileVersion, gitDestinationPath: projectFolder, isFromTemplateProject: false, portNumber, @@ -336,7 +338,7 @@ export function FromExistingGitRepo({ setCurrentView }) { )} - ) : recommendedDevfile.noRecommendation && !selectedDevfile ? ( + ) : recommendedDevfile.noRecommendation && !selectedDevfile? ( <> (undefined); + const [selectedDevfile, setSelectedDevfile] = React.useState(undefined); const [createComponentErrorMessage, setCreateComponentErrorMessage] = React.useState(''); + function getTargetPort(devfile: DevfileData): number { + return devfile?.components?.filter((component) => component.container) + .filter((component) => component.container.endpoints && component.container?.endpoints[0]) + .map((component) => component.container?.endpoints[0].targetPort) + .pop(); + }; + + function getComponentVersion(devfile: DevfileData): string { + return devfile?.metadata?.version ? devfile.metadata.version : 'latest'; + }; + + function getEffectiveDevfile(): DevfileData { + return selectedDevfile ? selectedDevfile : + recommendedDevfile && recommendedDevfile.devfile ? + recommendedDevfile.devfile : undefined; + }; + function respondToMessage(messageEvent: MessageEvent) { const message = messageEvent.data as Message; switch (message.action) { @@ -168,6 +185,14 @@ export function FromLocalCodebase(props: FromLocalCodebaseProps) { window.vscodeApi.postMessage({ action: 'getInitialWokspaceFolder' }); }, []); + React.useEffect(() => { + setPortNumber(getTargetPort(getEffectiveDevfile())); + window.vscodeApi.postMessage({ + action: 'validatePortNumber', + data: portNumber, + }); + }, [recommendedDevfile, selectedDevfile]); + function handleNext() { window.vscodeApi.postMessage({ action: 'getRecommendedDevfile', @@ -176,14 +201,15 @@ export function FromLocalCodebase(props: FromLocalCodebaseProps) { setRecommendedDevfile((prevState) => ({ ...prevState, isLoading: true })); } - function createComponentFromLocalCodebase(projectFolder: string, componentName: string, addToWorkspace: boolean, portNumber: number) { + function createComponentFromLocalCodebase(projectFolder: string, componentName: string, devfileVersion: string, addToWorkspace: boolean, portNumber: number) { window.vscodeApi.postMessage({ action: 'createComponent', data: { devfileDisplayName: selectedDevfile - ? selectedDevfile.name - : recommendedDevfile.devfile.name, + ? selectedDevfile.metadata.name + : recommendedDevfile.devfile.metadata.name, componentName, + devfileVersion, portNumber, path: projectFolder, isFromTemplateProject: false, @@ -209,7 +235,7 @@ export function FromLocalCodebase(props: FromLocalCodebaseProps) { setComponentName={setComponentName} /> { - portNumber && + portNumber !== undefined && {(recommendedDevfile.showRecommendation || - selectedDevfile) && ( + setSelectedDevfile) && ( { setCurrentPage('fromLocalCodeBase'); }} - setSelectedDevfile={setSelectedDevfile} + setSelectedDevfile={(_df) => { + setSelectedDevfile(_df); + }} /> ) : ( setCurrentPage('fromLocalCodeBase') diff --git a/src/webview/create-deployment/createDeploymentLoader.ts b/src/webview/create-deployment/createDeploymentLoader.ts index df6373c3a..4956d619e 100644 --- a/src/webview/create-deployment/createDeploymentLoader.ts +++ b/src/webview/create-deployment/createDeploymentLoader.ts @@ -10,6 +10,8 @@ import { promisify } from 'util'; import * as vscode from 'vscode'; import { extensions, Uri, ViewColumn, WebviewPanel, window } from 'vscode'; import { Alizer } from '../../alizer/alizerWrapper'; +import { AlizerAnalyzeResponse } from '../../alizer/types'; +import { Oc } from '../../oc/ocWrapper'; import { BuilderImage, BuilderImageWrapper, NormalizedBuilderImages } from '../../odo/builderImage'; import sendTelemetry from '../../telemetry'; import { ExtensionID } from '../../util/constants'; @@ -18,8 +20,6 @@ import { validatePortNumber } from '../common-ext/createComponentHelpers'; import { loadWebviewHtml, validateGitURL } from '../common-ext/utils'; -import { AlizerAnalyzeResponse } from '../../alizer/types'; -import { Oc } from '../../oc/ocWrapper'; interface CloneProcess { status: boolean; diff --git a/src/webview/create-route/app/createForm.tsx b/src/webview/create-route/app/createForm.tsx index ce2e4d117..1d3639cb1 100644 --- a/src/webview/create-route/app/createForm.tsx +++ b/src/webview/create-route/app/createForm.tsx @@ -2,31 +2,31 @@ * Copyright (c) Red Hat, Inc. All rights reserved. * Licensed under the MIT License. See LICENSE file in the project root for license information. *-----------------------------------------------------------------------------------------------*/ +import ArrowRightAltIcon from '@mui/icons-material/ArrowRightAlt'; import { Box, + Button, + Checkbox, Container, FormControl, - PaletteMode, - Stack, - ThemeProvider, - Typography, - TextField, + FormControlLabel, FormHelperText, + InputLabel, MenuItem, + PaletteMode, Select, - InputLabel, - Checkbox, - FormControlLabel, - Button + Stack, + TextField, + ThemeProvider, + Typography } from '@mui/material'; import * as React from 'react'; import 'react-dom'; import type { K8sResourceKind, Port } from '../../common/createServiceTypes'; -import type { RouteInputBoxText } from '../../common/route'; +import { ErrorPage } from '../../common/errorPage'; import { LoadScreen } from '../../common/loading'; -import ArrowRightAltIcon from '@mui/icons-material/ArrowRightAlt'; +import type { RouteInputBoxText } from '../../common/route'; import { createVSCodeTheme } from '../../common/vscode-theme'; -import { ErrorPage } from '../../common/errorPage'; /** * Component to select which type of service (which CRD) should be created. @@ -320,7 +320,7 @@ export function CreateService() { switch (page) { case 'Loading': - return ; + return ; case 'Error': pageElement = (); break; diff --git a/src/webview/create-service/app/createForm.tsx b/src/webview/create-service/app/createForm.tsx index 6a59ba8f8..5045777bf 100644 --- a/src/webview/create-service/app/createForm.tsx +++ b/src/webview/create-service/app/createForm.tsx @@ -2,6 +2,7 @@ * Copyright (c) Red Hat, Inc. All rights reserved. * Licensed under the MIT License. See LICENSE file in the project root for license information. *-----------------------------------------------------------------------------------------------*/ +import { ArrowBack } from '@mui/icons-material'; import ExpandLess from '@mui/icons-material/ExpandLess'; import ExpandMore from '@mui/icons-material/ExpandMore'; import { @@ -12,6 +13,7 @@ import { Container, FormControl, FormHelperText, + Grid, IconButton, InputLabel, MenuItem, @@ -21,28 +23,26 @@ import { Stack, ThemeProvider, Typography, - Grid, } from '@mui/material'; import Form from '@rjsf/mui'; import type { - ObjectFieldTemplateProps, - TitleFieldProps, + ArrayFieldTemplateItemType, ArrayFieldTemplateProps, + FormContextType, + ObjectFieldTemplateProps, RJSFSchema, StrictRJSFSchema, - FormContextType, - ArrayFieldTemplateItemType + TitleFieldProps } from '@rjsf/utils'; -import { getTemplate , getUiOptions} from '@rjsf/utils'; +import { getTemplate, getUiOptions } from '@rjsf/utils'; import validator from '@rjsf/validator-ajv8'; import * as React from 'react'; import 'react-dom'; +import { Converter } from 'showdown'; import type { CustomResourceDefinitionStub } from '../../common/createServiceTypes'; +import { ErrorPage } from '../../common/errorPage'; import { LoadScreen } from '../../common/loading'; import { createVSCodeTheme } from '../../common/vscode-theme'; -import { ArrowBack } from '@mui/icons-material'; -import { Converter } from 'showdown'; -import {ErrorPage} from '../../common/errorPage'; /** * A replacement for the RJSF object field component that resembles the one in Patternfly and allows collapsing. @@ -400,7 +400,7 @@ export function CreateService() { switch (page) { case 'Loading': - return ; + return ; case 'Error': return ; case 'PickServiceKind': diff --git a/src/webview/devfile-registry/registryViewLoader.ts b/src/webview/devfile-registry/registryViewLoader.ts index c0ddf2dc2..66c9dada3 100644 --- a/src/webview/devfile-registry/registryViewLoader.ts +++ b/src/webview/devfile-registry/registryViewLoader.ts @@ -7,12 +7,11 @@ import * as path from 'path'; import * as vscode from 'vscode'; import { Registry } from '../../odo/componentType'; import { Odo } from '../../odo/odoWrapper'; -import { ComponentTypesView } from '../../registriesView'; import sendTelemetry from '../../telemetry'; import { ExtensionID } from '../../util/constants'; import { getInitialWorkspaceFolder, selectWorkspaceFolder } from '../../util/workspace'; import { vsCommand } from '../../vscommand'; -import { getDevfileCapabilities, getDevfileRegistries, getDevfileTags, isValidProjectFolder, validateName, validatePortNumber } from '../common-ext/createComponentHelpers'; +import { isValidProjectFolder, sendDevfileForVersion, sendUpdatedCapabilities, sendUpdatedDevfileInfos, sendUpdatedRegistries, sendUpdatedTags, validateName, validatePortNumber } from '../common-ext/createComponentHelpers'; import { loadWebviewHtml } from '../common-ext/utils'; import { TemplateProjectIdentifier } from '../common/devfile'; @@ -31,22 +30,30 @@ async function devfileRegistryViewerMessageListener(event: any): Promise { switch (event?.action) { case 'init': void panel.webview.postMessage({ - action: 'setTheme', - themeValue: vscode.window.activeColorTheme.kind, - }) + action: 'setTheme', + themeValue: vscode.window.activeColorTheme.kind, + }); break; case 'getDevfileRegistries': - await RegistryViewLoader.sendUpdatedRegistries(); + await sendUpdatedRegistries(panel, RegistryViewLoader.url); + break; + case 'getDevfileInfos': + await sendUpdatedDevfileInfos(panel, RegistryViewLoader.url); + break; + case 'getDevfile': + await sendDevfileForVersion(panel, event?.data?.devfileInfo, event?.data?.version); break; case 'getDevfileCapabilities': - RegistryViewLoader.sendUpdatedCapabilities(); + sendUpdatedCapabilities(panel); break; case 'getDevfileTags': - await RegistryViewLoader.sendUpdatedTags(); + await sendUpdatedTags(panel, RegistryViewLoader.url); break; case 'createComponent': { const { projectFolder, componentName } = event.data; const templateProject: TemplateProjectIdentifier = event.data.templateProject; + const devfileVersion = event.data.devfileVersion && event.data.devfileVersion.length > 0 ? + event.data.devfileVersion : 'latest'; const portNumber: number = event.data.portNumber; const componentFolder = path.join(projectFolder, componentName); try { @@ -56,6 +63,7 @@ async function devfileRegistryViewerMessageListener(event: any): Promise { componentName, portNumber, templateProject.devfileId, + devfileVersion, templateProject.registryName, templateProject.templateProjectName, ); @@ -201,47 +209,4 @@ export default class RegistryViewLoader { panel?.dispose(); return Promise.resolve(); } - - static async sendUpdatedRegistries() { - if (panel) { - let registries = await getDevfileRegistries(); - if (RegistryViewLoader.url) { - registries = registries.filter((devfileRegistry) => devfileRegistry.url === RegistryViewLoader.url); - } - void panel.webview.postMessage({ - action: 'devfileRegistries', - data: registries, - }); - } - } - - static sendUpdatedCapabilities() { - if (panel) { - void panel.webview.postMessage({ - action: 'devfileCapabilities', - data: getDevfileCapabilities(), - }); - } - } - - static async sendUpdatedTags() { - if (panel) { - void panel.webview.postMessage({ - action: 'devfileTags', - data: await getDevfileTags(RegistryViewLoader.url), - }); - } - } } - -ComponentTypesView.instance.subject.subscribe(() => { - void RegistryViewLoader.sendUpdatedRegistries(); -}); - -ComponentTypesView.instance.subject.subscribe(() => { - RegistryViewLoader.sendUpdatedCapabilities(); -}); - -ComponentTypesView.instance.subject.subscribe(() => { - void RegistryViewLoader.sendUpdatedTags(); -}); diff --git a/src/webview/helm-chart/app/helmSearch.tsx b/src/webview/helm-chart/app/helmSearch.tsx index 9afdb0d4a..e4f6af151 100644 --- a/src/webview/helm-chart/app/helmSearch.tsx +++ b/src/webview/helm-chart/app/helmSearch.tsx @@ -3,15 +3,15 @@ * Licensed under the MIT License. See LICENSE file in the project root for license information. *-----------------------------------------------------------------------------------------------*/ -import React from 'react'; -import { Alert, Checkbox, Divider, FormControlLabel, FormGroup, IconButton, InputAdornment, Modal, Pagination, Stack, TextField, Theme, Tooltip, Typography } from '@mui/material'; import { Close, Search } from '@mui/icons-material'; -import { HelmListItem } from './helmListItem'; +import { Alert, Checkbox, Divider, FormControlLabel, FormGroup, IconButton, InputAdornment, Modal, Pagination, Stack, TextField, Theme, Tooltip, Typography } from '@mui/material'; +import { every } from 'lodash'; +import React from 'react'; import { ChartResponse, HelmRepo } from '../../../helm/helmChartType'; -import { VSCodeMessage } from '../vsCodeMessage'; import { LoadScreen } from '../../common/loading'; +import { VSCodeMessage } from '../vsCodeMessage'; +import { HelmListItem } from './helmListItem'; import { HelmModal } from './helmModal'; -import { every } from 'lodash'; declare module '@mui/material/SvgIcon' { interface SvgIconPropsColorOverrides { @@ -284,7 +284,7 @@ export function HelmSearch(props: HelmSearchProps) { { !isSomeHelmChartsRetrieved ? - : + : { (helmRepos.length > 1 || providerTypes.length > 1) && diff --git a/src/webview/invoke-serverless-function/app/invokeFunction.tsx b/src/webview/invoke-serverless-function/app/invokeFunction.tsx index aa6f5ad21..82edede25 100644 --- a/src/webview/invoke-serverless-function/app/invokeFunction.tsx +++ b/src/webview/invoke-serverless-function/app/invokeFunction.tsx @@ -2,7 +2,6 @@ * Copyright (c) Red Hat, Inc. All rights reserved. * Licensed under the MIT License. See LICENSE file in the project root for license information. *-----------------------------------------------------------------------------------------------*/ -import * as React from 'react'; import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import { Accordion, @@ -21,8 +20,9 @@ import { TextField, Typography, } from '@mui/material'; -import { InvokeFunctionPageProps } from '../../common/propertyTypes'; +import * as React from 'react'; import { LoadScreen } from '../../common/loading'; +import { InvokeFunctionPageProps } from '../../common/propertyTypes'; import './home.scss'; export const InvokeFunctionOrLoading = () => { @@ -89,7 +89,7 @@ export const InvokeFunctionOrLoading = () => { }); if (isLoading) { - return ; + return ; } return ( ); const resultPromise = Component.debug(devfileComponentItem2); const result = await resultPromise; @@ -445,7 +465,7 @@ suite('OpenShift/Component', function () { contextPath: comp1Folder, component: undefined, }; - sandbox.stub(Odo.prototype, 'getComponentTypes').resolves([]); + sandbox.stub(DevfileRegistry.prototype, 'getRegistryDevfileInfos').resolves([]); sandbox.stub(vscode.extensions, 'getExtension').returns({} as vscode.Extension); const resultPromise = Component.debug(devfileComponentItem2); const result = await resultPromise; @@ -461,7 +481,7 @@ suite('OpenShift/Component', function () { contextPath: comp1Folder, component: undefined, }; - sandbox.stub(Odo.prototype, 'getComponentTypes').resolves([]); + sandbox.stub(DevfileRegistry.prototype, 'getRegistryDevfileInfos').resolves([]); sandbox.stub(vscode.extensions, 'getExtension').returns({} as vscode.Extension); const resultPromise = Component.debug(devfileComponentItem2); let caughtError; @@ -482,7 +502,7 @@ suite('OpenShift/Component', function () { contextPath: comp1Folder, component: undefined, }; - sandbox.stub(Odo.prototype, 'getComponentTypes').resolves([]); + sandbox.stub(DevfileRegistry.prototype, 'getRegistryDevfileInfos').resolves([]); sandbox.stub(vscode.extensions, 'getExtension').returns({} as vscode.Extension); const resultPromise = Component.debug(devfileComponentItem2); let caughtError;